From a6fc5766c905b66ac63b12ee31cc1e4d9f47d884 Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:23:56 +0400 Subject: [PATCH 1/7] create working k8s setup --- Makefile | 15 +- cmd/{api => server}/main.go | 0 cmd/testrunner/main.go | 126 +++ cmd/worker/main.go | 119 +++ config/config.go | 59 +- go.mod | 155 +++- go.sum | 860 +++++++++++++++++- internal/api/response.go | 10 +- internal/queue/redis_test.go | 69 ++ .../20260216000001_create_test_runs.sql | 2 - .../storage/postgres/queries/test_runs.sql.go | 16 + internal/storage/postgres/schema/schema.sql | 2 - internal/storage/postgres/sqlc/test_runs.sql | 5 + internal/testrunner/client.go | 114 +++ internal/testrunner/db/db.go | 32 + internal/testrunner/db/models.go | 99 ++ internal/testrunner/db/seeder.sql.go | 128 +++ internal/testrunner/evm.go | 90 ++ internal/testrunner/evm_test.go | 47 + internal/testrunner/fixture.json | 20 + internal/testrunner/fixtures.go | 64 ++ internal/testrunner/jwt.go | 38 + internal/testrunner/jwt_test.go | 38 + internal/testrunner/queries/seeder.sql | 38 + internal/testrunner/schema/schema.sql | 47 + internal/testrunner/seeder.go | 302 ++++++ internal/testrunner/sqlc.yaml | 17 + internal/testrunner/tests.go | 472 ++++++++++ internal/types/test_run_test.go | 137 +++ internal/worker/artifacts.go | 67 ++ internal/worker/consumer.go | 138 +++ internal/worker/janitor.go | 109 +++ internal/worker/k8s.go | 382 ++++++++ internal/worker/k8s_test.go | 409 +++++++++ internal/worker/manifests.go | 475 ++++++++++ internal/worker/manifests_test.go | 199 ++++ internal/worker/naming.go | 90 ++ internal/worker/naming_test.go | 139 +++ internal/worker/runner.go | 282 ++++++ internal/worker/runner_test.go | 175 ++++ 40 files changed, 5549 insertions(+), 37 deletions(-) rename cmd/{api => server}/main.go (100%) create mode 100644 cmd/testrunner/main.go create mode 100644 cmd/worker/main.go create mode 100644 internal/queue/redis_test.go create mode 100644 internal/testrunner/client.go create mode 100644 internal/testrunner/db/db.go create mode 100644 internal/testrunner/db/models.go create mode 100644 internal/testrunner/db/seeder.sql.go create mode 100644 internal/testrunner/evm.go create mode 100644 internal/testrunner/evm_test.go create mode 100644 internal/testrunner/fixture.json create mode 100644 internal/testrunner/fixtures.go create mode 100644 internal/testrunner/jwt.go create mode 100644 internal/testrunner/jwt_test.go create mode 100644 internal/testrunner/queries/seeder.sql create mode 100644 internal/testrunner/schema/schema.sql create mode 100644 internal/testrunner/seeder.go create mode 100644 internal/testrunner/sqlc.yaml create mode 100644 internal/testrunner/tests.go create mode 100644 internal/types/test_run_test.go create mode 100644 internal/worker/artifacts.go create mode 100644 internal/worker/consumer.go create mode 100644 internal/worker/janitor.go create mode 100644 internal/worker/k8s.go create mode 100644 internal/worker/k8s_test.go create mode 100644 internal/worker/manifests.go create mode 100644 internal/worker/manifests_test.go create mode 100644 internal/worker/naming.go create mode 100644 internal/worker/naming_test.go create mode 100644 internal/worker/runner.go create mode 100644 internal/worker/runner_test.go diff --git a/Makefile b/Makefile index 977cafa..0cdbb4a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,13 @@ -.PHONY: build-api sqlc fmt lint +.PHONY: build-server build-worker build-testrunner sqlc fmt lint docker-testrunner -build-api: - go build -o bin/api ./cmd/api +build-server: + go build -o bin/server ./cmd/server + +build-worker: + go build -o bin/worker ./cmd/worker + +build-testrunner: + CGO_ENABLED=0 go build -o bin/testrunner ./cmd/testrunner sqlc: sqlc generate @@ -11,3 +17,6 @@ fmt: lint: golangci-lint run ./... + +docker-testrunner: + docker build --build-arg SERVICE=testrunner -t plugin-tests-testrunner:dev . diff --git a/cmd/api/main.go b/cmd/server/main.go similarity index 100% rename from cmd/api/main.go rename to cmd/server/main.go diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go new file mode 100644 index 0000000..97106a6 --- /dev/null +++ b/cmd/testrunner/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "os" + "time" + + "github.com/sirupsen/logrus" + + "github.com/vultisig/plugin-tests/internal/testrunner" +) + +var logger = logrus.New() + +func main() { + logger.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) + + if len(os.Args) < 2 { + logger.Fatal("usage: testrunner ") + } + + switch os.Args[1] { + case "seed": + runSeed() + case "test": + runTest() + default: + logger.Fatalf("unknown command: %s", os.Args[1]) + } +} + +func runSeed() { + ctx := context.Background() + + fixture, err := testrunner.LoadFixture() + if err != nil { + logger.WithError(err).Fatal("failed to load fixture") + } + + plugins := testrunner.GetTestPlugins() + + seeder := testrunner.NewSeeder(testrunner.SeederConfig{ + DSN: requireEnv("POSTGRES_DSN"), + S3: testrunner.S3Config{ + Endpoint: requireEnv("MINIO_ENDPOINT"), + Region: "us-east-1", + AccessKey: requireEnv("MINIO_ACCESS_KEY"), + SecretKey: requireEnv("MINIO_SECRET_KEY"), + Bucket: requireEnv("MINIO_BUCKET"), + }, + Fixture: fixture, + Plugins: plugins, + EncryptionSecret: requireEnv("ENCRYPTION_SECRET"), + }, logger) + + logger.Info("seeding database") + err = seeder.SeedDatabase(ctx) + if err != nil { + logger.WithError(err).Fatal("failed to seed database") + } + + logger.Info("seeding vaults to MinIO") + err = seeder.SeedVaults(ctx) + if err != nil { + logger.WithError(err).Fatal("failed to seed vaults") + } + + logger.Info("seeding completed successfully") +} + +func runTest() { + fixture, err := testrunner.LoadFixture() + if err != nil { + logger.WithError(err).Fatal("failed to load fixture") + } + + plugins := testrunner.GetTestPlugins() + verifierURL := requireEnv("VERIFIER_URL") + jwtSecret := requireEnv("JWT_SECRET") + + jwtToken, err := testrunner.GenerateJWT(jwtSecret, fixture.Vault.PublicKey, "integration-token-1", 24) + if err != nil { + logger.WithError(err).Fatal("failed to generate JWT") + } + + evmFixture, err := testrunner.GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "", 21000, 0) + if err != nil { + logger.WithError(err).Fatal("failed to generate EVM fixture") + } + + client := testrunner.NewTestClient(verifierURL) + + logger.WithField("verifier_url", verifierURL).Info("waiting for verifier health") + err = client.WaitForHealth(60 * time.Second) + if err != nil { + logger.WithError(err).Fatal("verifier not healthy") + } + logger.Info("verifier is healthy") + + suite := testrunner.NewTestSuite(client, fixture, plugins, jwtToken, evmFixture, logger) + suite.RunAll() + + if suite.Failed > 0 { + for _, e := range suite.Errors { + logger.WithField("error", e).Error("test failure") + } + logger.WithFields(logrus.Fields{ + "passed": suite.Passed, + "failed": suite.Failed, + "total": suite.Total, + }).Fatal("test suite failed") + } + + logger.WithFields(logrus.Fields{ + "passed": suite.Passed, + "total": suite.Total, + }).Info("all tests passed") +} + +func requireEnv(key string) string { + val := os.Getenv(key) + if val == "" { + logger.WithField("var", key).Fatal("required environment variable is not set") + } + return val +} diff --git a/cmd/worker/main.go b/cmd/worker/main.go new file mode 100644 index 0000000..7eeff54 --- /dev/null +++ b/cmd/worker/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/hibiken/asynq" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/vultisig/plugin-tests/config" + "github.com/vultisig/plugin-tests/internal/health" + "github.com/vultisig/plugin-tests/internal/logging" + "github.com/vultisig/plugin-tests/internal/queue" + "github.com/vultisig/plugin-tests/internal/storage/postgres" + "github.com/vultisig/plugin-tests/internal/worker" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + cancel() + }() + + cfg, err := config.ReadWorkerConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read config: %v\n", err) + os.Exit(1) + } + + logger := logging.NewLogger(cfg.LogFormat) + + db, err := postgres.NewPostgresBackend(cfg.Database.DSN) + if err != nil { + logger.WithError(err).Fatal("failed to connect to database") + } + defer db.Close() + + redisOpt, err := queue.NewRedisConnOpt(cfg.QueueRedis) + if err != nil { + logger.WithError(err).Fatal("failed to create redis connection") + } + + k8sClient, err := buildK8sClient(cfg.KubeConfig) + if err != nil { + logger.WithError(err).Fatal("failed to create k8s client") + } + + concurrency := cfg.Concurrency + if concurrency <= 0 { + concurrency = 1 + } + + srv := asynq.NewServer(redisOpt, asynq.Config{ + Concurrency: concurrency, + Queues: map[string]int{ + queue.QueueName: 1, + }, + Logger: logger, + }) + + go func() { + <-ctx.Done() + logger.Info("shutting down worker") + srv.Shutdown() + }() + + consumer := worker.NewConsumer(db, k8sClient, logger, cfg.K8sJob, cfg.ArtifactS3) + + janitor := worker.NewJanitor(db, k8sClient, logger, cfg.Janitor) + go janitor.Run(ctx) + + if cfg.HealthPort > 0 { + healthServer := health.New(cfg.HealthPort) + go func() { + err := healthServer.Start(ctx, logger) + if err != nil { + logger.WithError(err).Error("health server failed") + } + }() + } + + mux := asynq.NewServeMux() + mux.HandleFunc(queue.TypeRunIntegrationTest, consumer.Handle) + + logger.WithField("concurrency", concurrency).Info("starting worker") + + err = srv.Run(mux) + if err != nil { + logger.WithError(err).Fatal("worker failed") + } +} + +func buildK8sClient(kubeconfig string) (kubernetes.Interface, error) { + cfg, err := rest.InClusterConfig() + if err != nil { + if kubeconfig == "" { + return nil, fmt.Errorf("not in cluster and KUBECONFIG not set: %w", err) + } + cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, fmt.Errorf("failed to build kubeconfig from %s: %w", kubeconfig, err) + } + } + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes client: %w", err) + } + return client, nil +} diff --git a/config/config.go b/config/config.go index 174a9ae..20078ee 100644 --- a/config/config.go +++ b/config/config.go @@ -2,25 +2,29 @@ package config import ( "fmt" + "os" + "time" "github.com/kelseyhightower/envconfig" ) type APIConfig struct { - LogFormat string `envconfig:"LOG_FORMAT"` - Server ServerConfig `envconfig:"SERVER"` - Database DatabaseConfig - QueueRedis RedisConfig `envconfig:"QUEUE_REDIS"` + LogFormat string `envconfig:"LOG_FORMAT"` + Server ServerConfig `envconfig:"SERVER"` + Database DatabaseConfig `envconfig:"DATABASE"` + QueueRedis RedisConfig `envconfig:"QUEUE_REDIS"` } type WorkerConfig struct { - LogFormat string `envconfig:"LOG_FORMAT"` - Database DatabaseConfig - QueueRedis RedisConfig `envconfig:"QUEUE_REDIS"` - Kubernetes K8sConfig `envconfig:"KUBERNETES"` - ArtifactS3 S3Config `envconfig:"ARTIFACT_S3"` - Janitor JanitorConfig - HealthPort int `envconfig:"HEALTH_PORT"` + LogFormat string `envconfig:"LOG_FORMAT"` + Concurrency int `envconfig:"CONCURRENCY"` + KubeConfig string `envconfig:"KUBECONFIG"` + HealthPort int `envconfig:"HEALTH_PORT"` + Database DatabaseConfig `envconfig:"DATABASE"` + QueueRedis RedisConfig `envconfig:"QUEUE_REDIS"` + K8sJob K8sJobConfig `envconfig:"K8S"` + ArtifactS3 S3Config `envconfig:"ARTIFACT_S3"` + Janitor JanitorConfig `envconfig:"JANITOR"` } type ServerConfig struct { @@ -29,7 +33,7 @@ type ServerConfig struct { } type DatabaseConfig struct { - DSN string `envconfig:"DATABASE_DSN" required:"true"` + DSN string `envconfig:"DSN" required:"true"` } type RedisConfig struct { @@ -49,22 +53,30 @@ type S3Config struct { Bucket string `envconfig:"BUCKET"` } -type K8sConfig struct { - VerifierImage string `envconfig:"VERIFIER_IMAGE"` - VerifierWorkerImage string `envconfig:"VERIFIER_WORKER_IMAGE"` - TestImage string `envconfig:"TEST_IMAGE"` - ImagePullSecret string `envconfig:"IMAGE_PULL_SECRET"` - JobTimeoutMinutes int `envconfig:"JOB_TIMEOUT_MINUTES"` +type K8sJobConfig struct { + JobTimeout time.Duration `envconfig:"JOB_TIMEOUT"` + PollInterval time.Duration `envconfig:"POLL_INTERVAL"` + TTLAfterFinished int32 `envconfig:"JOB_TTL_SECONDS"` + VerifierImage string `envconfig:"VERIFIER_IMAGE"` + VerifierWorkerImage string `envconfig:"VERIFIER_WORKER_IMAGE"` + TestImage string `envconfig:"TEST_IMAGE"` + ImagePullSecret string `envconfig:"IMAGE_PULL_SECRET"` + PostgresImage string `envconfig:"POSTGRES_IMAGE"` + RedisImage string `envconfig:"REDIS_IMAGE"` + MinioImage string `envconfig:"MINIO_IMAGE"` + EncryptionSecret string `envconfig:"ENCRYPTION_SECRET"` + JWTSecret string `envconfig:"JWT_SECRET"` + PluginEndpoint string `envconfig:"PLUGIN_ENDPOINT"` } type JanitorConfig struct { - IntervalMinutes int `envconfig:"JANITOR_INTERVAL_MINUTES"` - StaleThresholdMinutes int `envconfig:"JANITOR_STALE_THRESHOLD_MINUTES"` + Interval time.Duration `envconfig:"INTERVAL"` + StaleThreshold time.Duration `envconfig:"STALE_THRESHOLD"` } func ReadAPIConfig() (*APIConfig, error) { var cfg APIConfig - err := envconfig.Process("", &cfg) + err := envconfig.Process("PLUGIN_TESTS_API", &cfg) if err != nil { return nil, fmt.Errorf("failed to read API config: %w", err) } @@ -73,9 +85,12 @@ func ReadAPIConfig() (*APIConfig, error) { func ReadWorkerConfig() (*WorkerConfig, error) { var cfg WorkerConfig - err := envconfig.Process("", &cfg) + err := envconfig.Process("PLUGIN_TESTS_WORKER", &cfg) if err != nil { return nil, fmt.Errorf("failed to read worker config: %w", err) } + if cfg.KubeConfig == "" { + cfg.KubeConfig = os.Getenv("KUBECONFIG") + } return &cfg, nil } diff --git a/go.mod b/go.mod index 6964f59..62e38e6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/vultisig/plugin-tests go 1.25.0 require ( + github.com/aws/aws-sdk-go v1.55.8 + github.com/ethereum/go-ethereum v1.15.11 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.26.0 github.com/jackc/pgx/v5 v5.8.0 @@ -10,30 +13,180 @@ require ( github.com/labstack/echo/v4 v4.15.0 github.com/pressly/goose/v3 v3.26.0 github.com/sirupsen/logrus v1.9.4 + github.com/stretchr/testify v1.11.1 + github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778 + github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292 + github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85 + google.golang.org/protobuf v1.36.10 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 ) require ( + cosmossdk.io/api v0.9.2 // indirect + cosmossdk.io/collections v1.2.1 // indirect + cosmossdk.io/core v0.11.3 // indirect + cosmossdk.io/errors v1.0.2 // indirect + cosmossdk.io/log v1.6.0 // indirect + cosmossdk.io/math v1.5.3 // indirect + cosmossdk.io/schema v1.1.0 // indirect + cosmossdk.io/store v1.1.2 // indirect + cosmossdk.io/x/tx v0.14.0 // indirect + github.com/DataDog/zstd v1.5.7 // indirect + github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/bnb-chain/tss-lib/v2 v2.0.2 // indirect + github.com/btcsuite/btcd v0.24.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cockroachdb/errors v1.12.0 // indirect + github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cometbft/cometbft v0.38.17 // indirect + github.com/cometbft/cometbft-db v0.14.1 // indirect + github.com/consensys/bavard v0.1.27 // indirect + github.com/consensys/gnark-crypto v0.16.0 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect + github.com/cosmos/cosmos-sdk v0.50.11 // indirect + github.com/cosmos/gogoproto v1.7.0 // indirect + github.com/cosmos/ics23/go v0.11.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getsentry/sentry-go v0.32.0 // indirect + github.com/go-kit/kit v0.13.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.3 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ipfs/go-log v1.0.5 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mfridman/interpolate v0.0.2 // 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.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/otiai10/primes v0.4.0 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/redis/go-redis/v9 v9.14.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/supranational/blst v0.3.14 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tidwall/btree v1.7.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect + go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.15.0 // indirect golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + rsc.io/tmplfunc v0.0.3 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace ( + github.com/agl/ed25519 => github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 + github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 + nhooyr.io/websocket => github.com/coder/websocket v1.8.6 ) diff --git a/go.sum b/go.sum index 40a4e76..2af1e8d 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,398 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.9.2 h1:9i9ptOBdmoIEVEVWLtYYHjxZonlF/aOVODLFaxpmNtg= +cosmossdk.io/api v0.9.2/go.mod h1:CWt31nVohvoPMTlPv+mMNCtC0a7BqRdESjCsstHcTkU= +cosmossdk.io/collections v1.2.1 h1:mAlNMs5vJwkda4TA+k5q/43p24RVAQ/qyDrjANu3BXE= +cosmossdk.io/collections v1.2.1/go.mod h1:PSsEJ/fqny0VPsHLFT6gXDj/2C1tBOTS9eByK0+PBFU= +cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= +cosmossdk.io/core v0.11.3/go.mod h1:9rL4RE1uDt5AJ4Tg55sYyHWXA16VmpHgbe0PbJc6N2Y= +cosmossdk.io/depinject v1.2.1 h1:eD6FxkIjlVaNZT+dXTQuwQTKZrFZ4UrfCq1RKgzyhMw= +cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM= +cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= +cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k= +cosmossdk.io/log v1.6.0 h1:SJIOmJ059wi1piyRgNRXKXhlDXGqnB5eQwhcZKv2tOk= +cosmossdk.io/log v1.6.0/go.mod h1:5cXXBvfBkR2/BcXmosdCSLXllvgSjphrrDVdfVRmBGM= +cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= +cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ= +cosmossdk.io/schema v1.1.0 h1:mmpuz3dzouCoyjjcMcA/xHBEmMChN+EHh8EHxHRHhzE= +cosmossdk.io/schema v1.1.0/go.mod h1:Gb7pqO+tpR+jLW5qDcNOSv0KtppYs7881kfzakguhhI= +cosmossdk.io/store v1.1.2 h1:3HOZG8+CuThREKv6cn3WSohAc6yccxO3hLzwK6rBC7o= +cosmossdk.io/store v1.1.2/go.mod h1:60rAGzTHevGm592kFhiUVkNC9w7gooSEn5iUBPzHQ6A= +cosmossdk.io/x/tx v0.14.0 h1:hB3O25kIcyDW/7kMTLMaO8Ripj3yqs5imceVd6c/heA= +cosmossdk.io/x/tx v0.14.0/go.mod h1:Tn30rSRA1PRfdGB3Yz55W4Sn6EIutr9xtMKSHij+9PM= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= +github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 h1:Vkf7rtHx8uHx8gDfkQaCdVfc+gfrF9v6sR6xJy7RXNg= +github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod h1:TnVqVdGEK8b6erOMkcyYGWzCQMw7HEMCOw3BgFYCFWs= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bnb-chain/tss-lib/v2 v2.0.2 h1:dL2GJFCSYsYQ0bHkGll+hNM2JWsC1rxDmJJJQEmUy9g= +github.com/bnb-chain/tss-lib/v2 v2.0.2/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +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.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +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/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/coder/websocket v1.8.6 h1:OmNKdwUvLj7VvHnl5o8skaVghSPLjWdHGCnFbkWqs9w= +github.com/coder/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +github.com/cometbft/cometbft v0.38.17 h1:FkrQNbAjiFqXydeAO81FUzriL4Bz0abYxN/eOHrQGOk= +github.com/cometbft/cometbft v0.38.17/go.mod h1:5l0SkgeLRXi6bBfQuevXjKqML1jjfJJlvI1Ulp02/o4= +github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= +github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/consensys/bavard v0.1.27 h1:j6hKUrGAy/H+gpNrpLU3I26n1yc+VMGmd6ID5+gAhOs= +github.com/consensys/bavard v0.1.27/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/gnark-crypto v0.16.0 h1:8Dl4eYmUWK9WmlP1Bj6je688gBRJCJbT8Mw4KoTAawo= +github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpLvzbcE2MqljY7diU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.11 h1:LxR1aAc8kixdrs3itO+3a44sFoc+vjxVAOyPFx22yjk= +github.com/cosmos/cosmos-sdk v0.50.11/go.mod h1:gt14Meok2IDCjbDtjwkbUcgVNEpUBDN/4hg9cCUtLgw= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= +github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= +github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= +github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= +github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= +github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= +github.com/crate-crypto/go-eth-kzg v1.3.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/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= +github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= +github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= +github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU= +github.com/ethereum/go-ethereum v1.15.11/go.mod h1:mf8YiHIb0GR4x4TipcvBUPxJLw1mFdmxzoDi11sDRoI= +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/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/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 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= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= +github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +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-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +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= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw= github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= +github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -27,76 +401,546 @@ github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +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= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24= github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= +github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +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= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +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/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +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/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/jsonindent v0.0.0-20171116142732-447bf004320b/go.mod h1:SXIpH2WO0dyF5YBc6Iq8jc8TEJYe1Fk2Rc1EVYUdIgY= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11/go.mod h1:1DmRMnU78i/OVkMnHzvhXSi4p8IhYUmtLJWhyOavJc0= +github.com/otiai10/primes v0.4.0 h1:RATMXrGz6bb8cs37DCDdHxbiJ8Jit3YLOD4wCDTkYEg= +github.com/otiai10/primes v0.4.0/go.mod h1:UrIZFvOIqbXG0dvYr0EiZ9iMd+RdUSc7qs1+UwuzkBk= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +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= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +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.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.14.1 h1:nDCrEiJmfOWhD76xlaw+HXT0c9hfNWeXgl0vIRYSDvQ= github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +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/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.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.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778 h1:XJ1hoo37JKGLmfxD4wYhXO8TJFBdUBnbxxK+zagJ4c4= +github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778/go.mod h1:UMc5q0Myab+BvzAe67UQrXTXwKGYNxK7bky7DJM+dl8= +github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 h1:goqwk4nQ/NEVIb3OPP9SUx7/u9ZfsUIcd5fIN/e4DVU= +github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74/go.mod h1:nOykk4nOy1L3yXtLSlYvVsgizBnCQ3tR2N5uwGPdvaM= +github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292 h1:CVXtJBOjJfzMsv+akQK65Jcup9/TSeMUXD8NMJ9O0eg= +github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292/go.mod h1:PUz2BoPkuCrk9EFeWavynFSIMM2DNULfFeSS0CGVIVc= +github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85 h1:NAVXp2Mm791Z/teQHUA8T7mhmx3bouyrXvQkLXvAu3M= +github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85/go.mod h1:jOf+2n1Eo/XZjiUbHjTfraPMw4HAZZ0Sw9Z6+vpQrU4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= +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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 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/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.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.8.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= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +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-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -105,3 +949,15 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/api/response.go b/internal/api/response.go index 53df07a..4108e8c 100644 --- a/internal/api/response.go +++ b/internal/api/response.go @@ -1,14 +1,14 @@ package api type SuccessResponse struct { - Data interface{} `json:"data"` + Data any `json:"data"` } type ListResponse struct { - Data interface{} `json:"data"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` + Data any `json:"data"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` } type ErrorResponse struct { diff --git a/internal/queue/redis_test.go b/internal/queue/redis_test.go new file mode 100644 index 0000000..3209038 --- /dev/null +++ b/internal/queue/redis_test.go @@ -0,0 +1,69 @@ +package queue + +import ( + "testing" + + "github.com/hibiken/asynq" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vultisig/plugin-tests/config" +) + +func TestNewRedisConnOpt(t *testing.T) { + t.Run("valid uri", func(t *testing.T) { + cfg := config.RedisConfig{URI: "redis://localhost:6379/0"} + + opt, err := NewRedisConnOpt(cfg) + require.NoError(t, err) + assert.NotNil(t, opt) + }) + + t.Run("invalid uri", func(t *testing.T) { + cfg := config.RedisConfig{URI: "not-a-valid-uri"} + + _, err := NewRedisConnOpt(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse redis URI") + }) + + t.Run("host port fallback", func(t *testing.T) { + cfg := config.RedisConfig{ + Host: "redis.local", + Port: "6380", + User: "admin", + Password: "secret", + DB: 2, + } + + opt, err := NewRedisConnOpt(cfg) + require.NoError(t, err) + + clientOpt, ok := opt.(asynq.RedisClientOpt) + require.True(t, ok) + assert.Equal(t, "redis.local:6380", clientOpt.Addr) + assert.Equal(t, "admin", clientOpt.Username) + assert.Equal(t, "secret", clientOpt.Password) + assert.Equal(t, 2, clientOpt.DB) + }) + + t.Run("neither uri nor host", func(t *testing.T) { + cfg := config.RedisConfig{} + + _, err := NewRedisConnOpt(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "requires either URI or Host") + }) + + t.Run("uri takes precedence over host", func(t *testing.T) { + cfg := config.RedisConfig{ + URI: "redis://localhost:6379/0", + Host: "other-host", + Port: "9999", + } + + opt, err := NewRedisConnOpt(cfg) + require.NoError(t, err) + assert.NotNil(t, opt) + }) +} diff --git a/internal/storage/postgres/migrations/20260216000001_create_test_runs.sql b/internal/storage/postgres/migrations/20260216000001_create_test_runs.sql index 6a6a9ae..26d7e3b 100644 --- a/internal/storage/postgres/migrations/20260216000001_create_test_runs.sql +++ b/internal/storage/postgres/migrations/20260216000001_create_test_runs.sql @@ -1,7 +1,5 @@ -- +goose Up -- +goose StatementBegin -CREATE EXTENSION IF NOT EXISTS pgcrypto; - CREATE TYPE test_run_status AS ENUM ('QUEUED','RUNNING','PASSED','FAILED','ERROR'); CREATE TABLE test_runs ( diff --git a/internal/storage/postgres/queries/test_runs.sql.go b/internal/storage/postgres/queries/test_runs.sql.go index ba6debc..811dbbc 100644 --- a/internal/storage/postgres/queries/test_runs.sql.go +++ b/internal/storage/postgres/queries/test_runs.sql.go @@ -169,6 +169,22 @@ func (q *Queries) ListTestRuns(ctx context.Context, arg *ListTestRunsParams) ([] return items, nil } +const markStaleRunAsError = `-- name: MarkStaleRunAsError :exec +UPDATE test_runs +SET status = 'ERROR', error_message = $2, finished_at = NOW(), updated_at = NOW() +WHERE id = $1 AND status = 'RUNNING' +` + +type MarkStaleRunAsErrorParams struct { + ID pgtype.UUID `json:"id"` + ErrorMessage pgtype.Text `json:"error_message"` +} + +func (q *Queries) MarkStaleRunAsError(ctx context.Context, arg *MarkStaleRunAsErrorParams) error { + _, err := q.db.Exec(ctx, markStaleRunAsError, arg.ID, arg.ErrorMessage) + return err +} + const updateTestRunFinished = `-- name: UpdateTestRunFinished :exec UPDATE test_runs SET status = $2, artifact_prefix = $3, error_message = $4, finished_at = NOW(), updated_at = NOW() diff --git a/internal/storage/postgres/schema/schema.sql b/internal/storage/postgres/schema/schema.sql index 3b7bc0f..941259c 100644 --- a/internal/storage/postgres/schema/schema.sql +++ b/internal/storage/postgres/schema/schema.sql @@ -1,5 +1,3 @@ -CREATE EXTENSION IF NOT EXISTS pgcrypto; - CREATE TYPE test_run_status AS ENUM ('QUEUED','RUNNING','PASSED','FAILED','ERROR'); CREATE TABLE test_runs ( diff --git a/internal/storage/postgres/sqlc/test_runs.sql b/internal/storage/postgres/sqlc/test_runs.sql index bbf5f4b..a5f6053 100644 --- a/internal/storage/postgres/sqlc/test_runs.sql +++ b/internal/storage/postgres/sqlc/test_runs.sql @@ -25,6 +25,11 @@ UPDATE test_runs SET status = $2, artifact_prefix = $3, error_message = $4, finished_at = NOW(), updated_at = NOW() WHERE id = $1; +-- name: MarkStaleRunAsError :exec +UPDATE test_runs +SET status = 'ERROR', error_message = $2, finished_at = NOW(), updated_at = NOW() +WHERE id = $1 AND status = 'RUNNING'; + -- name: GetStaleRunningRuns :many SELECT * FROM test_runs WHERE status = 'RUNNING' AND started_at < NOW() - $1::interval; diff --git a/internal/testrunner/client.go b/internal/testrunner/client.go new file mode 100644 index 0000000..2c757a9 --- /dev/null +++ b/internal/testrunner/client.go @@ -0,0 +1,114 @@ +package testrunner + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type TestClient struct { + baseURL string + httpClient *http.Client + jwtToken string + apiKey string +} + +func NewTestClient(baseURL string) *TestClient { + return &TestClient{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +func (c *TestClient) WithJWT(token string) *TestClient { + return &TestClient{ + baseURL: c.baseURL, + httpClient: c.httpClient, + jwtToken: token, + apiKey: c.apiKey, + } +} + +func (c *TestClient) WithAPIKey(key string) *TestClient { + return &TestClient{ + baseURL: c.baseURL, + httpClient: c.httpClient, + jwtToken: c.jwtToken, + apiKey: key, + } +} + +func (c *TestClient) GET(path string) (*http.Response, error) { + req, err := http.NewRequest(http.MethodGet, c.baseURL+path, nil) + if err != nil { + return nil, err + } + + c.setAuthHeaders(req) + return c.httpClient.Do(req) +} + +func (c *TestClient) POST(path string, body interface{}) (*http.Response, error) { + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(jsonBody) + } + + req, err := http.NewRequest(http.MethodPost, c.baseURL+path, bodyReader) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + c.setAuthHeaders(req) + return c.httpClient.Do(req) +} + +func (c *TestClient) setAuthHeaders(req *http.Request) { + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } else if c.jwtToken != "" { + req.Header.Set("Authorization", "Bearer "+c.jwtToken) + } +} + +func (c *TestClient) WaitForHealth(timeout time.Duration) error { + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + resp, err := c.GET("/plugins") + if err == nil { + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + } + time.Sleep(1 * time.Second) + } + + return fmt.Errorf("verifier not healthy after %v timeout", timeout) +} + +func ReadJSONResponse(resp *http.Response, v interface{}) error { + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + err = json.Unmarshal(body, v) + if err != nil { + return fmt.Errorf("failed to unmarshal response: %w", err) + } + + return nil +} diff --git a/internal/testrunner/db/db.go b/internal/testrunner/db/db.go new file mode 100644 index 0000000..9d485b5 --- /dev/null +++ b/internal/testrunner/db/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/testrunner/db/models.go b/internal/testrunner/db/models.go new file mode 100644 index 0000000..780dbdc --- /dev/null +++ b/internal/testrunner/db/models.go @@ -0,0 +1,99 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "database/sql/driver" + "fmt" + + "github.com/jackc/pgx/v5/pgtype" +) + +type PluginCategory string + +const ( + PluginCategoryAiAgent PluginCategory = "ai-agent" + PluginCategoryPlugin PluginCategory = "plugin" + PluginCategoryApp PluginCategory = "app" +) + +func (e *PluginCategory) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = PluginCategory(s) + case string: + *e = PluginCategory(s) + default: + return fmt.Errorf("unsupported scan type for PluginCategory: %T", src) + } + return nil +} + +type NullPluginCategory struct { + PluginCategory PluginCategory + Valid bool // Valid is true if PluginCategory is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullPluginCategory) Scan(value interface{}) error { + if value == nil { + ns.PluginCategory, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.PluginCategory.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullPluginCategory) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.PluginCategory), nil +} + +type Plugin struct { + ID interface{} + Title string + Description string + ServerEndpoint string + Category PluginCategory + Audited bool + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type PluginApikey struct { + ID pgtype.UUID + PluginID interface{} + Apikey string + CreatedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + Status int32 +} + +type PluginPolicy struct { + ID pgtype.UUID + PublicKey string + PluginID interface{} + PluginVersion string + PolicyVersion int32 + Signature string + Recipe string + Active bool + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type VaultToken struct { + ID pgtype.UUID + TokenID string + PublicKey string + CreatedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + LastUsedAt pgtype.Timestamptz + RevokedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} diff --git a/internal/testrunner/db/seeder.sql.go b/internal/testrunner/db/seeder.sql.go new file mode 100644 index 0000000..4b92fe4 --- /dev/null +++ b/internal/testrunner/db/seeder.sql.go @@ -0,0 +1,128 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: seeder.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const upsertPlugin = `-- name: UpsertPlugin :exec +INSERT INTO plugins (id, title, description, server_endpoint, category, audited) +VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + server_endpoint = EXCLUDED.server_endpoint, + category = EXCLUDED.category, + audited = EXCLUDED.audited, + updated_at = NOW() +` + +type UpsertPluginParams struct { + ID interface{} + Title string + Description string + ServerEndpoint string + Category PluginCategory + Audited bool +} + +func (q *Queries) UpsertPlugin(ctx context.Context, arg *UpsertPluginParams) error { + _, err := q.db.Exec(ctx, upsertPlugin, + arg.ID, + arg.Title, + arg.Description, + arg.ServerEndpoint, + arg.Category, + arg.Audited, + ) + return err +} + +const upsertPluginAPIKey = `-- name: UpsertPluginAPIKey :exec +INSERT INTO plugin_apikey (id, plugin_id, apikey, created_at, expires_at, status) +VALUES (gen_random_uuid(), $1, $2, NOW(), NULL, 1) +ON CONFLICT DO NOTHING +` + +type UpsertPluginAPIKeyParams struct { + PluginID interface{} + Apikey string +} + +func (q *Queries) UpsertPluginAPIKey(ctx context.Context, arg *UpsertPluginAPIKeyParams) error { + _, err := q.db.Exec(ctx, upsertPluginAPIKey, arg.PluginID, arg.Apikey) + return err +} + +const upsertPluginPolicy = `-- name: UpsertPluginPolicy :exec +INSERT INTO plugin_policies (id, public_key, plugin_id, plugin_version, policy_version, signature, recipe, active) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +ON CONFLICT (id) DO UPDATE SET + public_key = EXCLUDED.public_key, + plugin_id = EXCLUDED.plugin_id, + plugin_version = EXCLUDED.plugin_version, + policy_version = EXCLUDED.policy_version, + signature = EXCLUDED.signature, + recipe = EXCLUDED.recipe, + active = EXCLUDED.active, + updated_at = NOW() +` + +type UpsertPluginPolicyParams struct { + ID pgtype.UUID + PublicKey string + PluginID interface{} + PluginVersion string + PolicyVersion int32 + Signature string + Recipe string + Active bool +} + +func (q *Queries) UpsertPluginPolicy(ctx context.Context, arg *UpsertPluginPolicyParams) error { + _, err := q.db.Exec(ctx, upsertPluginPolicy, + arg.ID, + arg.PublicKey, + arg.PluginID, + arg.PluginVersion, + arg.PolicyVersion, + arg.Signature, + arg.Recipe, + arg.Active, + ) + return err +} + +const upsertVaultToken = `-- name: UpsertVaultToken :exec +INSERT INTO vault_tokens (token_id, public_key, expires_at, last_used_at) +VALUES ($1, $2, $3, $4) +ON CONFLICT (token_id) DO UPDATE SET + public_key = EXCLUDED.public_key, + expires_at = EXCLUDED.expires_at, + last_used_at = EXCLUDED.last_used_at, + revoked_at = NULL, + updated_at = NOW() +` + +type UpsertVaultTokenParams struct { + TokenID string + PublicKey string + ExpiresAt pgtype.Timestamptz + LastUsedAt pgtype.Timestamptz +} + +func (q *Queries) UpsertVaultToken(ctx context.Context, arg *UpsertVaultTokenParams) error { + _, err := q.db.Exec(ctx, upsertVaultToken, + arg.TokenID, + arg.PublicKey, + arg.ExpiresAt, + arg.LastUsedAt, + ) + return err +} diff --git a/internal/testrunner/evm.go b/internal/testrunner/evm.go new file mode 100644 index 0000000..da93848 --- /dev/null +++ b/internal/testrunner/evm.go @@ -0,0 +1,90 @@ +package testrunner + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +type EVMFixture struct { + TxB64 string + MsgB64 string + MsgSHA256B64 string +} + +type dynamicFeeTxWithoutSignature struct { + ChainID *big.Int + Nonce uint64 + GasTipCap *big.Int + GasFeeCap *big.Int + Gas uint64 + To *common.Address `rlp:"nil"` + Value *big.Int + Data []byte + AccessList ethtypes.AccessList +} + +func GenerateEVMFixture(chainID int64, to string, valueWei string, gas, nonce uint64) (*EVMFixture, error) { + chainIDBig := big.NewInt(chainID) + toAddr := common.HexToAddress(to) + + value := new(big.Int) + if valueWei != "" { + _, ok := value.SetString(valueWei, 10) + if !ok { + return nil, fmt.Errorf("invalid value: %q is not a valid base-10 integer", valueWei) + } + } else { + value.SetInt64(1000000000000000) + } + + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainIDBig, + Nonce: nonce, + GasTipCap: big.NewInt(1000000000), + GasFeeCap: big.NewInt(20000000000), + Gas: gas, + To: &toAddr, + Value: value, + Data: nil, + }) + + unsignedTx := dynamicFeeTxWithoutSignature{ + ChainID: chainIDBig, + Nonce: nonce, + GasTipCap: big.NewInt(1000000000), + GasFeeCap: big.NewInt(20000000000), + Gas: gas, + To: &toAddr, + Value: value, + Data: nil, + AccessList: ethtypes.AccessList{}, + } + + txBytes, err := rlp.EncodeToBytes(unsignedTx) + if err != nil { + return nil, fmt.Errorf("failed to RLP encode: %w", err) + } + + typedTxBytes := append([]byte{byte(ethtypes.DynamicFeeTxType)}, txBytes...) + txB64 := base64.StdEncoding.EncodeToString(typedTxBytes) + + signer := ethtypes.LatestSignerForChainID(chainIDBig) + hash := signer.Hash(tx) + msgBytes := hash.Bytes() + msgB64 := base64.StdEncoding.EncodeToString(msgBytes) + + msgSha256 := sha256.Sum256(msgBytes) + msgSha256B64 := base64.StdEncoding.EncodeToString(msgSha256[:]) + + return &EVMFixture{ + TxB64: txB64, + MsgB64: msgB64, + MsgSHA256B64: msgSha256B64, + }, nil +} diff --git a/internal/testrunner/evm_test.go b/internal/testrunner/evm_test.go new file mode 100644 index 0000000..3924440 --- /dev/null +++ b/internal/testrunner/evm_test.go @@ -0,0 +1,47 @@ +package testrunner + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateEVMFixture_HappyPath(t *testing.T) { + fixture, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "", 21000, 0) + require.NoError(t, err) + + assert.NotEmpty(t, fixture.TxB64) + assert.NotEmpty(t, fixture.MsgB64) + assert.NotEmpty(t, fixture.MsgSHA256B64) + + txBytes, err := base64.StdEncoding.DecodeString(fixture.TxB64) + require.NoError(t, err) + assert.Equal(t, byte(0x02), txBytes[0]) + + msgBytes, err := base64.StdEncoding.DecodeString(fixture.MsgB64) + require.NoError(t, err) + assert.Len(t, msgBytes, 32) + + hashBytes, err := base64.StdEncoding.DecodeString(fixture.MsgSHA256B64) + require.NoError(t, err) + assert.Len(t, hashBytes, 32) +} + +func TestGenerateEVMFixture_Deterministic(t *testing.T) { + f1, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "1000000", 21000, 5) + require.NoError(t, err) + + f2, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "1000000", 21000, 5) + require.NoError(t, err) + + assert.Equal(t, f1.TxB64, f2.TxB64) + assert.Equal(t, f1.MsgB64, f2.MsgB64) + assert.Equal(t, f1.MsgSHA256B64, f2.MsgSHA256B64) +} + +func TestGenerateEVMFixture_InvalidValue(t *testing.T) { + _, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "not-a-number", 21000, 0) + assert.Error(t, err) +} diff --git a/internal/testrunner/fixture.json b/internal/testrunner/fixture.json new file mode 100644 index 0000000..7d0f39c --- /dev/null +++ b/internal/testrunner/fixture.json @@ -0,0 +1,20 @@ +{ + "vault": { + "public_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "name": "integration-test-vault", + "created_at": "2025-12-22T00:00:00Z", + "vault_b64": "CAEStANVeks5NXNMb1k0OWxyTjdzUHprekRQQndtdEx3UktWelFUajFsc3BUbTI3VVM3OG9lS3ZKR0djRmQ1RGViZVlYbzRpR3M3d2dXRFBnaEYySms4NWZ4bW9GZTU4ZWp6UjV5S1BVcnFZM0h5di9ydVN4SE5tVENmT2NLbElMVFBodGVpckFOaTZBMFFNeTYrakFqL3JrY0FQTFZ3cmFTS1h6VG84ZUxoTHE0aXZKQWM3Mkt1Yld1Rmw1YjFMUndaUURVOVQxK1A0YnVWT3NGNGNTK1pBaWZhQWppOG9UbHFxR2Vwb1NyQUdabGJOa2FWWFYrbFJmclY5STZjaFlsaHZ4c0RvR0NxdUZabTZiUEw4NVo0QWN0U1A0N0I2SVFOK0RFYlAvaWF4dVZXY1hqMkxJV2hiUnZZQTJ6OHRCWjhyRWF4U3BqWk1DK1pyd0dXbGxrd3I4WWFkLy9RRVF4WFQ1UWFxb0FmVjZMcmphT096dUhwck4yRmJJbjdWRWozdlFYOWZsSmZTVXBMZ3hwdHdUcmQxWXV6cHlMVXVRZ25ReXRhTlRyZEt3c0t4WXNDdzRNdz09GAE=" + }, + "reshare": { + "session_id": "00000000-0000-0000-0000-000000000000", + "hex_encryption_key": "0000000000000000000000000000000000000000000000000000000000000000", + "hex_chain_code": "0000000000000000000000000000000000000000000000000000000000000000", + "local_party_id": "verifier-test-party", + "old_parties": [ + "party1", + "party2" + ], + "old_reshare_prefix": "integration-test", + "email": "integration@test.example.com" + } +} diff --git a/internal/testrunner/fixtures.go b/internal/testrunner/fixtures.go new file mode 100644 index 0000000..7fac47b --- /dev/null +++ b/internal/testrunner/fixtures.go @@ -0,0 +1,64 @@ +package testrunner + +import ( + _ "embed" + "encoding/json" + "fmt" + "os" +) + +//go:embed fixture.json +var fixtureJSON []byte + +type FixtureData struct { + Vault struct { + PublicKey string `json:"public_key"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + VaultB64 string `json:"vault_b64"` + } `json:"vault"` + Reshare struct { + SessionID string `json:"session_id"` + HexEncryptionKey string `json:"hex_encryption_key"` + HexChainCode string `json:"hex_chain_code"` + LocalPartyID string `json:"local_party_id"` + OldParties []string `json:"old_parties"` + OldResharePrefix string `json:"old_reshare_prefix"` + Email string `json:"email"` + } `json:"reshare"` +} + +type PluginConfig struct { + ID string + Title string + Description string + ServerEndpoint string + Category string + Audited bool +} + +func LoadFixture() (*FixtureData, error) { + var fixture FixtureData + err := json.Unmarshal(fixtureJSON, &fixture) + if err != nil { + return nil, fmt.Errorf("failed to parse embedded fixture JSON: %w", err) + } + return &fixture, nil +} + +func GetTestPlugins() []PluginConfig { + pluginEndpoint := os.Getenv("PLUGIN_ENDPOINT") + if pluginEndpoint == "" { + pluginEndpoint = "http://localhost:8082" + } + + return []PluginConfig{ + { + ID: "vultisig-dca-0000", + Title: "DCA (Dollar Cost Averaging)", + Description: "Automated recurring swaps and transfers", + ServerEndpoint: pluginEndpoint, + Category: "app", + }, + } +} diff --git a/internal/testrunner/jwt.go b/internal/testrunner/jwt.go new file mode 100644 index 0000000..cbbb508 --- /dev/null +++ b/internal/testrunner/jwt.go @@ -0,0 +1,38 @@ +package testrunner + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + PublicKey string `json:"public_key"` + TokenID string `json:"token_id"` + jwt.RegisteredClaims +} + +func GenerateJWT(secret, pubkey, tokenID string, expireHours int) (string, error) { + if secret == "" || pubkey == "" { + return "", fmt.Errorf("secret and pubkey are required") + } + + expirationTime := time.Now().Add(time.Duration(expireHours) * time.Hour) + claims := &Claims{ + PublicKey: pubkey, + TokenID: tokenID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", fmt.Errorf("failed to create token: %w", err) + } + + return tokenString, nil +} diff --git a/internal/testrunner/jwt_test.go b/internal/testrunner/jwt_test.go new file mode 100644 index 0000000..ffe607b --- /dev/null +++ b/internal/testrunner/jwt_test.go @@ -0,0 +1,38 @@ +package testrunner + +import ( + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateJWT_HappyPath(t *testing.T) { + secret := "test-secret" + pubkey := "03abc123" + tokenID := "token-1" + + tokenStr, err := GenerateJWT(secret, pubkey, tokenID, 24) + require.NoError(t, err) + assert.NotEmpty(t, tokenStr) + + claims := &Claims{} + parsed, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + require.NoError(t, err) + assert.True(t, parsed.Valid) + assert.Equal(t, pubkey, claims.PublicKey) + assert.Equal(t, tokenID, claims.TokenID) +} + +func TestGenerateJWT_EmptySecret(t *testing.T) { + _, err := GenerateJWT("", "pubkey", "token", 24) + assert.Error(t, err) +} + +func TestGenerateJWT_EmptyPubkey(t *testing.T) { + _, err := GenerateJWT("secret", "", "token", 24) + assert.Error(t, err) +} diff --git a/internal/testrunner/queries/seeder.sql b/internal/testrunner/queries/seeder.sql new file mode 100644 index 0000000..7959afe --- /dev/null +++ b/internal/testrunner/queries/seeder.sql @@ -0,0 +1,38 @@ +-- name: UpsertPlugin :exec +INSERT INTO plugins (id, title, description, server_endpoint, category, audited) +VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + server_endpoint = EXCLUDED.server_endpoint, + category = EXCLUDED.category, + audited = EXCLUDED.audited, + updated_at = NOW(); + +-- name: UpsertPluginAPIKey :exec +INSERT INTO plugin_apikey (id, plugin_id, apikey, created_at, expires_at, status) +VALUES (gen_random_uuid(), $1, $2, NOW(), NULL, 1) +ON CONFLICT DO NOTHING; + +-- name: UpsertVaultToken :exec +INSERT INTO vault_tokens (token_id, public_key, expires_at, last_used_at) +VALUES ($1, $2, $3, $4) +ON CONFLICT (token_id) DO UPDATE SET + public_key = EXCLUDED.public_key, + expires_at = EXCLUDED.expires_at, + last_used_at = EXCLUDED.last_used_at, + revoked_at = NULL, + updated_at = NOW(); + +-- name: UpsertPluginPolicy :exec +INSERT INTO plugin_policies (id, public_key, plugin_id, plugin_version, policy_version, signature, recipe, active) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +ON CONFLICT (id) DO UPDATE SET + public_key = EXCLUDED.public_key, + plugin_id = EXCLUDED.plugin_id, + plugin_version = EXCLUDED.plugin_version, + policy_version = EXCLUDED.policy_version, + signature = EXCLUDED.signature, + recipe = EXCLUDED.recipe, + active = EXCLUDED.active, + updated_at = NOW(); diff --git a/internal/testrunner/schema/schema.sql b/internal/testrunner/schema/schema.sql new file mode 100644 index 0000000..63ec2f5 --- /dev/null +++ b/internal/testrunner/schema/schema.sql @@ -0,0 +1,47 @@ +CREATE DOMAIN plugin_id AS TEXT; + +CREATE TYPE plugin_category AS ENUM ('ai-agent', 'plugin', 'app'); + +CREATE TABLE plugins ( + id plugin_id PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL DEFAULT '', + server_endpoint TEXT NOT NULL, + category plugin_category NOT NULL, + audited BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE plugin_apikey ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plugin_id plugin_id NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + apikey TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NULL, + status INT NOT NULL DEFAULT 1 CHECK (status IN (0, 1)) +); + +CREATE TABLE vault_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token_id VARCHAR(255) NOT NULL UNIQUE, + public_key VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + last_used_at TIMESTAMPTZ, + revoked_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE plugin_policies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + public_key TEXT NOT NULL, + plugin_id plugin_id NOT NULL, + plugin_version TEXT NOT NULL, + policy_version INTEGER NOT NULL, + signature TEXT NOT NULL, + recipe TEXT NOT NULL, + active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/internal/testrunner/seeder.go b/internal/testrunner/seeder.go new file mode 100644 index 0000000..f561189 --- /dev/null +++ b/internal/testrunner/seeder.go @@ -0,0 +1,302 @@ +package testrunner + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "time" + + "github.com/sirupsen/logrus" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" + recipetypes "github.com/vultisig/recipes/types" + "github.com/vultisig/vultisig-go/common" + "google.golang.org/protobuf/proto" + + "github.com/vultisig/plugin-tests/internal/testrunner/db" +) + +type S3Config struct { + Endpoint string + Region string + AccessKey string + SecretKey string + Bucket string +} + +type SeederConfig struct { + DSN string + S3 S3Config + Fixture *FixtureData + Plugins []PluginConfig + EncryptionSecret string +} + +type Seeder struct { + config SeederConfig + logger *logrus.Logger +} + +func NewSeeder(cfg SeederConfig, logger *logrus.Logger) *Seeder { + return &Seeder{config: cfg, logger: logger} +} + +func (s *Seeder) SeedDatabase(ctx context.Context) error { + pool, err := pgxpool.New(ctx, s.config.DSN) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + defer pool.Close() + + tx, err := pool.Begin(ctx) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + + defer func() { + if r := recover(); r != nil { + _ = tx.Rollback(ctx) + panic(r) + } + }() + + queries := db.New(pool).WithTx(tx) + + s.logger.Info("seeding integration database") + + for _, plugin := range s.config.Plugins { + s.logger.WithField("plugin_id", plugin.ID).Info("inserting plugin") + + err = queries.UpsertPlugin(ctx, &db.UpsertPluginParams{ + ID: plugin.ID, + Title: plugin.Title, + Description: plugin.Description, + ServerEndpoint: plugin.ServerEndpoint, + Category: db.PluginCategory(plugin.Category), + Audited: plugin.Audited, + }) + if err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to insert plugin %s: %w", plugin.ID, err) + } + + apiKey := fmt.Sprintf("integration-test-apikey-%s", plugin.ID) + err = queries.UpsertPluginAPIKey(ctx, &db.UpsertPluginAPIKeyParams{ + PluginID: plugin.ID, + Apikey: apiKey, + }) + if err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to insert API key for plugin %s: %w", plugin.ID, err) + } + + s.logger.WithField("plugin_id", plugin.ID).Info("plugin seeded") + } + + vaultPubkey := s.config.Fixture.Vault.PublicKey + if vaultPubkey != "" { + tokenID := "integration-token-1" + now := time.Now() + expiresAt := now.Add(365 * 24 * time.Hour) + + s.logger.Info("inserting vault token") + + err = queries.UpsertVaultToken(ctx, &db.UpsertVaultTokenParams{ + TokenID: tokenID, + PublicKey: vaultPubkey, + ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true}, + LastUsedAt: pgtype.Timestamptz{Time: now, Valid: true}, + }) + if err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to insert vault token: %w", err) + } + + s.logger.Info("inserting test policies") + + targetAddr := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" + permissiveRecipe, recipeErr := generatePermissivePolicy(targetAddr) + if recipeErr != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to generate permissive policy: %w", recipeErr) + } + + for i, plugin := range s.config.Plugins { + policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) + parsed, parseErr := uuid.Parse(policyID) + if parseErr != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to parse policy UUID %s: %w", policyID, parseErr) + } + + err = queries.UpsertPluginPolicy(ctx, &db.UpsertPluginPolicyParams{ + ID: pgtype.UUID{Bytes: parsed, Valid: true}, + PublicKey: vaultPubkey, + PluginID: plugin.ID, + PluginVersion: "1.0.0", + PolicyVersion: 1, + Signature: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + Recipe: permissiveRecipe, + Active: true, + }) + if err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to insert policy for plugin %s: %w", plugin.ID, err) + } + + s.logger.WithFields(logrus.Fields{ + "policy_id": policyID, + "plugin_id": plugin.ID, + }).Info("policy seeded") + } + } + + err = tx.Commit(ctx) + if err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + s.logger.Info("database seeding completed") + return nil +} + +func (s *Seeder) SeedVaults(ctx context.Context) error { + vaultData, err := base64.StdEncoding.DecodeString(s.config.Fixture.Vault.VaultB64) + if err != nil { + return fmt.Errorf("failed to decode vault_b64: %w", err) + } + + encryptedVault, err := common.EncryptVault(s.config.EncryptionSecret, vaultData) + if err != nil { + return fmt.Errorf("failed to encrypt vault: %w", err) + } + + vaultContainer := &vaultType.VaultContainer{ + Version: 1, + Vault: base64.StdEncoding.EncodeToString(encryptedVault), + IsEncrypted: true, + } + + containerBytes, err := proto.Marshal(vaultContainer) + if err != nil { + return fmt.Errorf("failed to marshal vault container: %w", err) + } + + vaultBackup := []byte(base64.StdEncoding.EncodeToString(containerBytes)) + + sess, err := session.NewSession(&aws.Config{ + Endpoint: aws.String(s.config.S3.Endpoint), + Region: aws.String(s.config.S3.Region), + Credentials: credentials.NewStaticCredentials(s.config.S3.AccessKey, s.config.S3.SecretKey, ""), + S3ForcePathStyle: aws.Bool(true), + }) + if err != nil { + return fmt.Errorf("failed to create S3 session: %w", err) + } + + s3Client := s3.New(sess) + + s.logger.Info("seeding vault fixtures to MinIO") + + for _, plugin := range s.config.Plugins { + key := fmt.Sprintf("%s-%s.vult", plugin.ID, s.config.Fixture.Vault.PublicKey) + + _, err = s3Client.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(s.config.S3.Bucket), + Key: aws.String(key), + Body: bytes.NewReader(vaultBackup), + ContentType: aws.String("application/octet-stream"), + }) + if err != nil { + return fmt.Errorf("failed to upload %s: %w", key, err) + } + + s.logger.WithField("key", key).Info("uploaded vault file") + } + + billingKey := fmt.Sprintf("vultisig-fees-feee-%s.vult", s.config.Fixture.Vault.PublicKey) + _, err = s3Client.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(s.config.S3.Bucket), + Key: aws.String(billingKey), + Body: bytes.NewReader(vaultBackup), + ContentType: aws.String("application/octet-stream"), + }) + if err != nil { + return fmt.Errorf("failed to upload billing vault %s: %w", billingKey, err) + } + + s.logger.WithField("key", billingKey).Info("uploaded billing vault") + s.logger.Info("vault seeding completed") + return nil +} + +func generatePermissivePolicy(targetAddr string) (string, error) { + policy := &recipetypes.Policy{ + Id: "permissive-test-policy", + Name: "Permissive Test Policy", + Description: "Allows all transactions for testing", + Version: 1, + Author: "integration-test", + Rules: []*recipetypes.Rule{ + { + Id: "allow-ethereum-eth-transfer", + Resource: "ethereum.eth.transfer", + Effect: recipetypes.Effect_EFFECT_ALLOW, + Description: "Allow Ethereum transfers", + Target: &recipetypes.Target{ + TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, + Target: &recipetypes.Target_Address{ + Address: targetAddr, + }, + }, + ParameterConstraints: []*recipetypes.ParameterConstraint{ + { + ParameterName: "amount", + Constraint: &recipetypes.Constraint{ + Type: recipetypes.ConstraintType_CONSTRAINT_TYPE_ANY, + }, + }, + }, + }, + { + Id: "allow-ethereum-erc20-transfer", + Resource: "ethereum.erc20.transfer", + Effect: recipetypes.Effect_EFFECT_ALLOW, + Description: "Allow ERC20 transfers", + Target: &recipetypes.Target{ + TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, + Target: &recipetypes.Target_Address{ + Address: targetAddr, + }, + }, + }, + { + Id: "allow-ethereum-erc20-approve", + Resource: "ethereum.erc20.approve", + Effect: recipetypes.Effect_EFFECT_ALLOW, + Description: "Allow ERC20 approvals", + Target: &recipetypes.Target{ + TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, + Target: &recipetypes.Target_Address{ + Address: targetAddr, + }, + }, + }, + }, + } + + data, err := proto.Marshal(policy) + if err != nil { + return "", fmt.Errorf("failed to marshal policy: %w", err) + } + + return base64.StdEncoding.EncodeToString(data), nil +} diff --git a/internal/testrunner/sqlc.yaml b/internal/testrunner/sqlc.yaml new file mode 100644 index 0000000..5e725e6 --- /dev/null +++ b/internal/testrunner/sqlc.yaml @@ -0,0 +1,17 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "queries" + schema: "schema" + gen: + go: + package: "db" + out: "db" + sql_package: "pgx/v5" + emit_json_tags: false + emit_prepared_queries: false + emit_interface: false + emit_exact_table_names: false + emit_empty_slices: true + emit_result_struct_pointers: true + emit_params_struct_pointers: true diff --git a/internal/testrunner/tests.go b/internal/testrunner/tests.go new file mode 100644 index 0000000..4d18902 --- /dev/null +++ b/internal/testrunner/tests.go @@ -0,0 +1,472 @@ +package testrunner + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +type TestSuite struct { + client *TestClient + fixture *FixtureData + plugins []PluginConfig + jwtToken string + evmFixture *EVMFixture + logger *logrus.Logger + Passed int + Failed int + Total int + Errors []string +} + +func NewTestSuite(client *TestClient, fixture *FixtureData, plugins []PluginConfig, jwtToken string, evmFixture *EVMFixture, logger *logrus.Logger) *TestSuite { + return &TestSuite{ + client: client, + fixture: fixture, + plugins: plugins, + jwtToken: jwtToken, + evmFixture: evmFixture, + logger: logger, + } +} + +func (s *TestSuite) run(name string, fn func() error) { + s.Total++ + log := s.logger.WithFields(logrus.Fields{ + "test": name, + "index": s.Total, + }) + log.Info("running") + + err := fn() + if err != nil { + s.Failed++ + errStr := fmt.Sprintf("%s: %s", name, err) + s.Errors = append(s.Errors, errStr) + log.WithError(err).Error("FAIL") + } else { + s.Passed++ + log.Info("PASS") + } +} + +func (s *TestSuite) RunAll() { + sections := []struct { + name string + fn func() + }{ + {"plugin endpoints", s.testPluginEndpoints}, + {"vault endpoints", s.testVaultEndpoints}, + {"policy endpoints", s.testPolicyEndpoints}, + {"signer endpoints", s.testSignerEndpoints}, + } + + for _, section := range sections { + beforePassed := s.Passed + beforeFailed := s.Failed + + s.logger.WithField("section", section.name).Info("starting section") + section.fn() + + sectionPassed := s.Passed - beforePassed + sectionFailed := s.Failed - beforeFailed + s.logger.WithFields(logrus.Fields{ + "section": section.name, + "passed": sectionPassed, + "failed": sectionFailed, + }).Info("section completed") + } + + s.logger.WithFields(logrus.Fields{ + "total": s.Total, + "passed": s.Passed, + "failed": s.Failed, + }).Info("all sections completed") +} + +func (s *TestSuite) testPluginEndpoints() { + for _, plugin := range s.plugins { + pluginID := plugin.ID + + s.run(pluginID+"/GetPluginDetails", func() error { + resp, err := s.client.GET("/plugins/" + pluginID) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + + var apiResp struct { + Data struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + if apiResp.Data.ID != pluginID { + return fmt.Errorf("expected plugin ID %s, got %s", pluginID, apiResp.Data.ID) + } + if apiResp.Data.Title == "" { + return fmt.Errorf("expected non-empty title") + } + return nil + }) + + s.run(pluginID+"/GetRecipeSpecification", func() error { + resp, err := s.client.GET("/plugins/" + pluginID + "/recipe-specification") + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + + var apiResp struct { + Data struct { + PluginID string `json:"plugin_id"` + PluginName string `json:"plugin_name"` + } `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + if apiResp.Data.PluginID != pluginID { + return fmt.Errorf("expected plugin_id %s, got %s", pluginID, apiResp.Data.PluginID) + } + if apiResp.Data.PluginName == "" { + return fmt.Errorf("expected non-empty plugin_name") + } + return nil + }) + } +} + +func (s *TestSuite) testVaultEndpoints() { + time.Sleep(2 * time.Second) + + for _, plugin := range s.plugins { + pluginID := plugin.ID + pubkey := s.fixture.Vault.PublicKey + + time.Sleep(500 * time.Millisecond) + + s.run(pluginID+"/VaultExists", func() error { + resp, err := s.client.WithJWT(s.jwtToken).GET("/vault/exist/" + pluginID + "/" + pubkey) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + + var apiResp struct { + Data string `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + if apiResp.Data != "ok" { + return fmt.Errorf("expected data 'ok', got '%s'", apiResp.Data) + } + return nil + }) + + s.run(pluginID+"/GetVault_HappyPath", func() error { + time.Sleep(500 * time.Millisecond) + resp, err := s.client.WithJWT(s.jwtToken).GET("/vault/get/" + pluginID + "/" + pubkey) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + return nil + }) + } +} + +func (s *TestSuite) testPolicyEndpoints() { + for i, plugin := range s.plugins { + pluginID := plugin.ID + policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) + + s.run(pluginID+"/GetPolicy_HappyPath", func() error { + resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policy/" + policyID) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + + var apiResp struct { + Data struct { + ID string `json:"id"` + PluginID string `json:"plugin_id"` + Active bool `json:"active"` + } `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + if apiResp.Data.ID != policyID { + return fmt.Errorf("expected policy ID %s, got %s", policyID, apiResp.Data.ID) + } + if apiResp.Data.PluginID != pluginID { + return fmt.Errorf("expected plugin ID %s, got %s", pluginID, apiResp.Data.PluginID) + } + if !apiResp.Data.Active { + return fmt.Errorf("expected policy to be active") + } + return nil + }) + + s.run(pluginID+"/GetAllPolicies_HappyPath", func() error { + resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policies/" + pluginID) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/CreatePolicy_InvalidSignature", func() error { + reqBody := map[string]interface{}{ + "id": "00000000-0000-0000-0000-000000000001", + "public_key": s.fixture.Vault.PublicKey, + "plugin_id": pluginID, + "plugin_version": "1.0.0", + "policy_version": 1, + "signature": "0x" + strings.Repeat("0", 130), + "recipe": "CgA=", + "billing": []interface{}{}, + "active": true, + } + + resp, err := s.client.WithJWT(s.jwtToken).POST("/plugin/policy", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + return fmt.Errorf("expected status 400, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/CreatePolicy_NoAuth", func() error { + reqBody := map[string]interface{}{ + "id": "00000000-0000-0000-0000-000000000001", + "public_key": s.fixture.Vault.PublicKey, + "plugin_id": pluginID, + "plugin_version": "1.0.0", + "policy_version": 1, + "signature": "0x" + strings.Repeat("0", 130), + "recipe": "CgA=", + "billing": []interface{}{}, + "active": true, + } + + resp, err := s.client.POST("/plugin/policy", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("expected status 401, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/GetPolicy_NoAuth", func() error { + resp, err := s.client.GET("/plugin/policy/test-id") + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("expected status 401, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/GetPolicy_InvalidID", func() error { + resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policy/test-id") + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + return fmt.Errorf("expected status 400, got %d", resp.StatusCode) + } + return nil + }) + } +} + +func (s *TestSuite) testSignerEndpoints() { + for i, plugin := range s.plugins { + pluginID := plugin.ID + apiKey := fmt.Sprintf("integration-test-apikey-%s", pluginID) + policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) + + if i > 0 { + time.Sleep(2 * time.Second) + } + + s.run(pluginID+"/Sign_NoAPIKey", func() error { + reqBody := map[string]interface{}{ + "plugin_id": pluginID, + "public_key": s.fixture.Vault.PublicKey, + "policy_id": policyID, + "messages": []interface{}{}, + } + + resp, err := s.client.POST("/plugin-signer/sign", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("expected status 401, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/Sign_InvalidAPIKey", func() error { + reqBody := map[string]interface{}{ + "plugin_id": pluginID, + "public_key": s.fixture.Vault.PublicKey, + "policy_id": policyID, + "messages": []interface{}{}, + } + + resp, err := s.client.WithAPIKey("invalid-api-key").POST("/plugin-signer/sign", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("expected status 401, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/Sign_EmptyMessages", func() error { + reqBody := map[string]interface{}{ + "plugin_id": pluginID, + "public_key": s.fixture.Vault.PublicKey, + "policy_id": policyID, + "messages": []interface{}{}, + } + + resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + return fmt.Errorf("expected status 400, got %d", resp.StatusCode) + } + return nil + }) + + s.run(pluginID+"/Sign_ValidRequest", func() error { + reqBody := map[string]interface{}{ + "plugin_id": pluginID, + "public_key": s.fixture.Vault.PublicKey, + "policy_id": policyID, + "transactions": s.evmFixture.TxB64, + "transaction_type": "evm", + "messages": []map[string]interface{}{ + { + "message": s.evmFixture.MsgB64, + "chain": "Ethereum", + "hash": s.evmFixture.MsgSHA256B64, + "hash_function": "SHA256", + }, + }, + } + + resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + } + + var apiResp struct { + Data struct { + TaskIDs []string `json:"task_ids"` + } `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + + if len(apiResp.Data.TaskIDs) != 1 { + return fmt.Errorf("expected 1 task_id, got %d", len(apiResp.Data.TaskIDs)) + } + + taskID := apiResp.Data.TaskIDs[0] + return s.verifySignResponse(apiKey, taskID) + }) + } +} + +func (s *TestSuite) verifySignResponse(apiKey, taskID string) error { + resp, err := s.client.GET("/plugin-signer/sign/response/" + taskID) + if err != nil { + return fmt.Errorf("GetSignResponse_NoAPIKey request failed: %w", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("GetSignResponse_NoAPIKey: expected status 401, got %d", resp.StatusCode) + } + + resp, err = s.client.WithAPIKey(apiKey).GET("/plugin-signer/sign/response/" + taskID) + if err != nil { + return fmt.Errorf("GetSignResponse_WithAPIKey request failed: %w", err) + } + resp.Body.Close() + if resp.StatusCode < 200 { + return fmt.Errorf("GetSignResponse_WithAPIKey: expected status >= 200, got %d", resp.StatusCode) + } + + return nil +} diff --git a/internal/types/test_run_test.go b/internal/types/test_run_test.go new file mode 100644 index 0000000..9eb237a --- /dev/null +++ b/internal/types/test_run_test.go @@ -0,0 +1,137 @@ +package types + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vultisig/plugin-tests/internal/storage/postgres/queries" +) + +func TestTestRunFromQuery(t *testing.T) { + fixedTime := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + fixedUUID := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") + + t.Run("all fields valid", func(t *testing.T) { + q := &queries.TestRun{ + ID: pgtype.UUID{Bytes: fixedUUID, Valid: true}, + PluginID: "my-plugin", + ProposalID: pgtype.Text{String: "prop-123", Valid: true}, + Version: pgtype.Text{String: "v1.0.0", Valid: true}, + Status: queries.TestRunStatusPASSED, + RequestedBy: "tester", + ArtifactPrefix: pgtype.Text{String: "/artifacts/123", Valid: true}, + ErrorMessage: pgtype.Text{String: "some error", Valid: true}, + StartedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + FinishedAt: pgtype.Timestamptz{Time: fixedTime.Add(time.Hour), Valid: true}, + CreatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + UpdatedAt: pgtype.Timestamptz{Time: fixedTime.Add(time.Hour), Valid: true}, + } + + result := TestRunFromQuery(q) + + assert.Equal(t, fixedUUID, result.ID) + assert.Equal(t, "my-plugin", result.PluginID) + assert.Equal(t, TestRunStatus("PASSED"), result.Status) + assert.Equal(t, "tester", result.RequestedBy) + + require.NotNil(t, result.ProposalID) + assert.Equal(t, "prop-123", *result.ProposalID) + + require.NotNil(t, result.Version) + assert.Equal(t, "v1.0.0", *result.Version) + + require.NotNil(t, result.ArtifactPrefix) + assert.Equal(t, "/artifacts/123", *result.ArtifactPrefix) + + require.NotNil(t, result.ErrorMessage) + assert.Equal(t, "some error", *result.ErrorMessage) + + require.NotNil(t, result.StartedAt) + assert.Equal(t, fixedTime, *result.StartedAt) + + require.NotNil(t, result.FinishedAt) + assert.Equal(t, fixedTime.Add(time.Hour), *result.FinishedAt) + + assert.Equal(t, fixedTime, result.CreatedAt) + assert.Equal(t, fixedTime.Add(time.Hour), result.UpdatedAt) + }) + + t.Run("all nullable fields invalid", func(t *testing.T) { + q := &queries.TestRun{ + ID: pgtype.UUID{Bytes: fixedUUID, Valid: true}, + PluginID: "plugin", + Status: queries.TestRunStatusQUEUED, + RequestedBy: "user", + CreatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + UpdatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + } + + result := TestRunFromQuery(q) + + assert.Nil(t, result.ProposalID) + assert.Nil(t, result.Version) + assert.Nil(t, result.ArtifactPrefix) + assert.Nil(t, result.ErrorMessage) + assert.Nil(t, result.StartedAt) + assert.Nil(t, result.FinishedAt) + }) + + t.Run("partial nulls", func(t *testing.T) { + q := &queries.TestRun{ + ID: pgtype.UUID{Bytes: fixedUUID, Valid: true}, + PluginID: "plugin", + ProposalID: pgtype.Text{String: "prop-1", Valid: true}, + Status: queries.TestRunStatusRUNNING, + RequestedBy: "user", + ErrorMessage: pgtype.Text{String: "err", Valid: true}, + CreatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + UpdatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + } + + result := TestRunFromQuery(q) + + require.NotNil(t, result.ProposalID) + assert.Equal(t, "prop-1", *result.ProposalID) + + assert.Nil(t, result.Version) + + require.NotNil(t, result.ErrorMessage) + assert.Equal(t, "err", *result.ErrorMessage) + + assert.Nil(t, result.ArtifactPrefix) + assert.Nil(t, result.StartedAt) + assert.Nil(t, result.FinishedAt) + }) + + t.Run("id invalid", func(t *testing.T) { + q := &queries.TestRun{ + ID: pgtype.UUID{Valid: false}, + PluginID: "plugin", + Status: queries.TestRunStatusQUEUED, + RequestedBy: "user", + CreatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + UpdatedAt: pgtype.Timestamptz{Time: fixedTime, Valid: true}, + } + + result := TestRunFromQuery(q) + assert.Equal(t, uuid.Nil, result.ID) + }) + + t.Run("timestamps invalid", func(t *testing.T) { + q := &queries.TestRun{ + ID: pgtype.UUID{Bytes: fixedUUID, Valid: true}, + PluginID: "plugin", + Status: queries.TestRunStatusQUEUED, + RequestedBy: "user", + } + + result := TestRunFromQuery(q) + assert.True(t, result.CreatedAt.IsZero()) + assert.True(t, result.UpdatedAt.IsZero()) + }) +} diff --git a/internal/worker/artifacts.go b/internal/worker/artifacts.go new file mode 100644 index 0000000..8be32fb --- /dev/null +++ b/internal/worker/artifacts.go @@ -0,0 +1,67 @@ +package worker + +import ( + "bytes" + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + + "github.com/vultisig/plugin-tests/config" +) + +type ArtifactUploader struct { + cfg config.S3Config +} + +func NewArtifactUploader(cfg config.S3Config) *ArtifactUploader { + return &ArtifactUploader{cfg: cfg} +} + +func (u *ArtifactUploader) UploadRunArtifacts(ctx context.Context, runID string, result *RunResult) (string, error) { + if u.cfg.Bucket == "" { + return "", nil + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(u.cfg.Region), + Endpoint: aws.String(u.cfg.Endpoint), + Credentials: credentials.NewStaticCredentials(u.cfg.AccessKey, u.cfg.SecretKey, ""), + S3ForcePathStyle: aws.Bool(true), + }) + if err != nil { + return "", fmt.Errorf("failed to create S3 session: %w", err) + } + + client := s3.New(sess) + prefix := runID + + if result.SeederLogs != "" { + err = u.upload(ctx, client, prefix+"/seeder.txt", result.SeederLogs) + if err != nil { + return prefix, fmt.Errorf("failed to upload seeder logs: %w", err) + } + } + + if result.TestLogs != "" { + err = u.upload(ctx, client, prefix+"/test.txt", result.TestLogs) + if err != nil { + return prefix, fmt.Errorf("failed to upload test logs: %w", err) + } + } + + return prefix, nil +} + +func (u *ArtifactUploader) upload(ctx context.Context, client *s3.S3, key, content string) error { + _, err := client.PutObjectWithContext(ctx, &s3.PutObjectInput{ + Bucket: aws.String(u.cfg.Bucket), + Key: aws.String(key), + Body: bytes.NewReader([]byte(content)), + ContentType: aws.String("text/plain"), + }) + return err +} diff --git a/internal/worker/consumer.go b/internal/worker/consumer.go new file mode 100644 index 0000000..c70e3fb --- /dev/null +++ b/internal/worker/consumer.go @@ -0,0 +1,138 @@ +package worker + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/jackc/pgx/v5/pgtype" + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + + "github.com/vultisig/plugin-tests/config" + "github.com/vultisig/plugin-tests/internal/queue" + "github.com/vultisig/plugin-tests/internal/storage" + "github.com/vultisig/plugin-tests/internal/storage/postgres/queries" +) + +const maxErrorMessageLen = 4096 + +type Consumer struct { + db storage.DatabaseStorage + k8s kubernetes.Interface + logger *logrus.Logger + cfg config.K8sJobConfig + artifactCfg config.S3Config +} + +func NewConsumer(db storage.DatabaseStorage, k8s kubernetes.Interface, logger *logrus.Logger, cfg config.K8sJobConfig, artifactCfg config.S3Config) *Consumer { + return &Consumer{ + db: db, + k8s: k8s, + logger: logger, + cfg: cfg, + artifactCfg: artifactCfg, + } +} + +func (c *Consumer) Handle(ctx context.Context, t *asynq.Task) error { + var payload queue.TestRunPayload + err := json.Unmarshal(t.Payload(), &payload) + if err != nil { + return fmt.Errorf("failed to unmarshal payload: %w", err) + } + + parsed, err := uuid.Parse(payload.RunID) + if err != nil { + return fmt.Errorf("invalid run_id %s: %w", payload.RunID, err) + } + pgID := pgtype.UUID{Bytes: parsed, Valid: true} + + nsName := namespaceNameWithPlugin(payload.RunID, payload.PluginID) + labels := runLabels(payload.RunID, payload.PluginID, kindIntegration) + + log := c.logger.WithFields(logrus.Fields{ + "run_id": payload.RunID, + "plugin_id": payload.PluginID, + "namespace": nsName, + }) + log.Info("processing test run") + + err = c.db.Queries().UpdateTestRunStarted(ctx, pgID) + if err != nil { + return fmt.Errorf("failed to mark run as started: %w", err) + } + + createdNS := false + defer func() { + if !createdNS { + return + } + cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + delErr := deleteNamespace(cleanupCtx, c.k8s, nsName) + if delErr != nil { + log.WithError(delErr).Warn("failed to cleanup namespace") + } else { + log.Info("namespace cleaned up") + } + }() + + err = createNamespace(ctx, c.k8s, nsName, labels) + if err != nil { + c.finishWithError(ctx, pgID, err.Error(), log) + return nil + } + createdNS = true + log.Info("namespace created") + + runner := NewRunner(c.k8s, c.cfg, log) + result := runner.Run(ctx, nsName, payload.RunID, payload.PluginID, labels) + + uploader := NewArtifactUploader(c.artifactCfg) + artifactPrefix, uploadErr := uploader.UploadRunArtifacts(ctx, payload.RunID, result) + if uploadErr != nil { + log.WithError(uploadErr).Warn("failed to upload artifacts") + } + + if result.ErrorMsg != "" { + c.finishWithArtifacts(ctx, pgID, queries.TestRunStatusERROR, result.ErrorMsg, artifactPrefix, log) + return nil + } + + if result.Passed { + c.finishWithArtifacts(ctx, pgID, queries.TestRunStatusPASSED, "", artifactPrefix, log) + log.Info("test run passed") + } else { + c.finishWithArtifacts(ctx, pgID, queries.TestRunStatusFAILED, result.ErrorMsg, artifactPrefix, log) + log.Warn("test run failed") + } + return nil +} + +func (c *Consumer) finishWithError(ctx context.Context, id pgtype.UUID, errMsg string, log *logrus.Entry) { + c.finishWithArtifacts(ctx, id, queries.TestRunStatusERROR, errMsg, "", log) +} + +func (c *Consumer) finishWithArtifacts(ctx context.Context, id pgtype.UUID, status queries.TestRunStatus, errMsg, artifactPrefix string, log *logrus.Entry) { + if len(errMsg) > maxErrorMessageLen { + errMsg = errMsg[:maxErrorMessageLen] + } + params := &queries.UpdateTestRunFinishedParams{ + ID: id, + Status: status, + } + if errMsg != "" { + params.ErrorMessage = pgtype.Text{String: errMsg, Valid: true} + } + if artifactPrefix != "" { + params.ArtifactPrefix = pgtype.Text{String: artifactPrefix, Valid: true} + } + err := c.db.Queries().UpdateTestRunFinished(ctx, params) + if err != nil { + log.WithError(err).Error("failed to update test run status") + } +} diff --git a/internal/worker/janitor.go b/internal/worker/janitor.go new file mode 100644 index 0000000..fe46ab0 --- /dev/null +++ b/internal/worker/janitor.go @@ -0,0 +1,109 @@ +package worker + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/vultisig/plugin-tests/config" + "github.com/vultisig/plugin-tests/internal/storage" + "github.com/vultisig/plugin-tests/internal/storage/postgres/queries" +) + +type Janitor struct { + db storage.DatabaseStorage + k8s kubernetes.Interface + logger *logrus.Logger + cfg config.JanitorConfig +} + +func NewJanitor(db storage.DatabaseStorage, k8s kubernetes.Interface, logger *logrus.Logger, cfg config.JanitorConfig) *Janitor { + return &Janitor{ + db: db, + k8s: k8s, + logger: logger, + cfg: cfg, + } +} + +func (j *Janitor) Run(ctx context.Context) { + if j.cfg.Interval <= 0 || j.cfg.StaleThreshold <= 0 { + j.logger.Warn("janitor disabled (interval or stale_threshold <= 0)") + return + } + + ticker := time.NewTicker(j.cfg.Interval) + defer ticker.Stop() + + j.logger.WithFields(logrus.Fields{ + "interval": j.cfg.Interval, + "stale_threshold": j.cfg.StaleThreshold, + }).Info("janitor started") + + for { + select { + case <-ctx.Done(): + j.logger.Info("janitor stopped") + return + case <-ticker.C: + j.sweep(ctx) + } + } +} + +func (j *Janitor) sweep(ctx context.Context) { + j.cleanStaleNamespaces(ctx) + j.markStaleRuns(ctx) +} + +func (j *Janitor) cleanStaleNamespaces(ctx context.Context) { + nsList, err := j.k8s.CoreV1().Namespaces().List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", labelManagedBy, managedByValue), + }) + if err != nil { + j.logger.WithError(err).Error("janitor: failed to list namespaces") + return + } + + now := time.Now() + threshold := now.Add(-j.cfg.StaleThreshold) + for _, ns := range nsList.Items { + if ns.CreationTimestamp.Time.Before(threshold) { + j.logger.WithField("namespace", ns.Name).Info("janitor: deleting stale namespace") + delCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + err = deleteNamespace(delCtx, j.k8s, ns.Name) + cancel() + if err != nil { + j.logger.WithError(err).WithField("namespace", ns.Name).Warn("janitor: failed to delete namespace") + } + } + } +} + +func (j *Janitor) markStaleRuns(ctx context.Context) { + interval := pgtype.Interval{ + Microseconds: j.cfg.StaleThreshold.Microseconds(), + Valid: true, + } + staleRuns, err := j.db.Queries().GetStaleRunningRuns(ctx, interval) + if err != nil { + j.logger.WithError(err).Error("janitor: failed to get stale runs") + return + } + + for _, run := range staleRuns { + j.logger.WithField("run_id", run.ID).Info("janitor: marking stale run as ERROR") + err = j.db.Queries().MarkStaleRunAsError(ctx, &queries.MarkStaleRunAsErrorParams{ + ID: run.ID, + ErrorMessage: pgtype.Text{String: "marked as stale by janitor", Valid: true}, + }) + if err != nil { + j.logger.WithError(err).WithField("run_id", run.ID).Warn("janitor: failed to mark run as error") + } + } +} diff --git a/internal/worker/k8s.go b/internal/worker/k8s.go new file mode 100644 index 0000000..c315579 --- /dev/null +++ b/internal/worker/k8s.go @@ -0,0 +1,382 @@ +package worker + +import ( + "context" + "fmt" + "io" + "time" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" +) + +const ( + defaultDummyImage = "busybox:1.36" + maxLogBytes = 1 << 20 // 1MB +) + +func createNamespace(ctx context.Context, clientset kubernetes.Interface, name string, labels map[string]string) error { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + } + _, err := clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create namespace %s: %w", name, err) + } + return nil +} + +func createDenyAllNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string) error { + dnsPort := intstr.FromInt32(53) + udp := corev1.ProtocolUDP + tcp := corev1.ProtocolTCP + + policy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deny-all", + Namespace: namespace, + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{}, + PolicyTypes: []networkingv1.PolicyType{ + networkingv1.PolicyTypeIngress, + networkingv1.PolicyTypeEgress, + }, + Egress: []networkingv1.NetworkPolicyEgressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &udp, Port: &dnsPort}, + {Protocol: &tcp, Port: &dnsPort}, + }, + }, + }, + }, + } + _, err := clientset.NetworkingV1().NetworkPolicies(namespace).Create(ctx, policy, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create network policy in %s: %w", namespace, err) + } + return nil +} + +func deleteNamespace(ctx context.Context, clientset kubernetes.Interface, name string) error { + propagation := metav1.DeletePropagationForeground + err := clientset.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{ + PropagationPolicy: &propagation, + }) + if err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to delete namespace %s: %w", name, err) + } + return nil +} + +func createDummyJob(ctx context.Context, clientset kubernetes.Interface, namespace string, name string, runID string, labels map[string]string, ttlSeconds int32) (*batchv1.Job, error) { + var backoffLimit int32 + var ttlPtr *int32 + if ttlSeconds > 0 { + ttlPtr = &ttlSeconds + } + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: &backoffLimit, + TTLSecondsAfterFinished: ttlPtr, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "dummy", + Image: defaultDummyImage, + Command: []string{"sh", "-c", "echo \"integration test dummy for run $RUN_ID\" && sleep 1"}, + Env: []corev1.EnvVar{ + {Name: "RUN_ID", Value: runID}, + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("32Mi"), + }, + }, + }, + }, + }, + }, + }, + } + + created, err := clientset.BatchV1().Jobs(namespace).Create(ctx, job, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + existing, err := clientset.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("job already exists but failed to get it %s: %w", name, err) + } + return existing, nil + } + return nil, fmt.Errorf("failed to create job %s: %w", name, err) + } + return created, nil +} + +func waitForJob(ctx context.Context, clientset kubernetes.Interface, namespace string, name string, timeout time.Duration, poll time.Duration) (bool, error) { + if poll <= 0 { + poll = 2 * time.Second + } + if timeout <= 0 { + timeout = 5 * time.Minute + } + + deadline := time.After(timeout) + ticker := time.NewTicker(poll) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return false, ctx.Err() + case <-deadline: + return false, fmt.Errorf("timeout waiting for job %s/%s after %v", namespace, name, timeout) + case <-ticker.C: + job, err := clientset.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + return false, fmt.Errorf("failed to get job %s/%s: %w", namespace, name, err) + } + if job.Status.Succeeded > 0 { + return true, nil + } + if job.Status.Failed > 0 { + return false, nil + } + } + } +} + +func fetchJobLogs(ctx context.Context, clientset kubernetes.Interface, namespace string, name string, maxRetries int, retryDelay time.Duration) (string, error) { + return fetchJobLogsByContainer(ctx, clientset, namespace, name, "dummy", maxRetries, retryDelay) +} + +func createIntraNamespaceNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string) error { + dnsPort := intstr.FromInt32(53) + httpsPort := intstr.FromInt32(443) + udp := corev1.ProtocolUDP + tcp := corev1.ProtocolTCP + + policy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-intra-namespace", + Namespace: namespace, + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{}, + PolicyTypes: []networkingv1.PolicyType{ + networkingv1.PolicyTypeIngress, + networkingv1.PolicyTypeEgress, + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + {PodSelector: &metav1.LabelSelector{}}, + }, + }, + }, + Egress: []networkingv1.NetworkPolicyEgressRule{ + { + To: []networkingv1.NetworkPolicyPeer{ + {PodSelector: &metav1.LabelSelector{}}, + }, + }, + { + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &udp, Port: &dnsPort}, + {Protocol: &tcp, Port: &dnsPort}, + }, + }, + { + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &tcp, Port: &httpsPort}, + }, + }, + }, + }, + } + _, err := clientset.NetworkingV1().NetworkPolicies(namespace).Create(ctx, policy, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create network policy in %s: %w", namespace, err) + } + return nil +} + +func applyDeployment(ctx context.Context, clientset kubernetes.Interface, dep *appsv1.Deployment) error { + _, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create deployment %s: %w", dep.Name, err) + } + return nil +} + +func applyService(ctx context.Context, clientset kubernetes.Interface, svc *corev1.Service) error { + _, err := clientset.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create service %s: %w", svc.Name, err) + } + return nil +} + +func applyConfigMap(ctx context.Context, clientset kubernetes.Interface, cm *corev1.ConfigMap) error { + _, err := clientset.CoreV1().ConfigMaps(cm.Namespace).Create(ctx, cm, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create configmap %s: %w", cm.Name, err) + } + return nil +} + +func applyJob(ctx context.Context, clientset kubernetes.Interface, job *batchv1.Job) (*batchv1.Job, error) { + created, err := clientset.BatchV1().Jobs(job.Namespace).Create(ctx, job, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + existing, getErr := clientset.BatchV1().Jobs(job.Namespace).Get(ctx, job.Name, metav1.GetOptions{}) + if getErr != nil { + return nil, fmt.Errorf("job already exists but failed to get %s: %w", job.Name, getErr) + } + return existing, nil + } + return nil, fmt.Errorf("failed to create job %s: %w", job.Name, err) + } + return created, nil +} + +func waitForDeploymentReady(ctx context.Context, clientset kubernetes.Interface, namespace, name string, timeout, poll time.Duration) error { + if poll <= 0 { + poll = 2 * time.Second + } + if timeout <= 0 { + timeout = 5 * time.Minute + } + + deadline := time.After(timeout) + ticker := time.NewTicker(poll) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline: + return fmt.Errorf("timeout waiting for deployment %s/%s after %v", namespace, name, timeout) + case <-ticker.C: + dep, err := clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + continue + } + return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, name, err) + } + if dep.Status.ReadyReplicas >= 1 { + return nil + } + } + } +} + +func fetchJobLogsByContainer(ctx context.Context, clientset kubernetes.Interface, namespace, jobName, containerName string, maxRetries int, retryDelay time.Duration) (string, error) { + if maxRetries <= 0 { + maxRetries = 1 + } + if retryDelay <= 0 { + retryDelay = 1 * time.Second + } + + var pods *corev1.PodList + var err error + + for attempt := 0; attempt < maxRetries; attempt++ { + pods, err = clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("job-name=%s", jobName), + }) + if err != nil { + return "", fmt.Errorf("failed to list pods for job %s: %w", jobName, err) + } + if len(pods.Items) > 0 { + break + } + if attempt < maxRetries-1 { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(retryDelay): + } + } + } + + if len(pods.Items) == 0 { + return "", fmt.Errorf("no pods found for job %s after %d attempts", jobName, maxRetries) + } + + newest := pods.Items[0] + for _, pod := range pods.Items[1:] { + if pod.CreationTimestamp.After(newest.CreationTimestamp.Time) { + newest = pod + } + } + + stream, err := clientset.CoreV1().Pods(namespace).GetLogs(newest.Name, &corev1.PodLogOptions{ + Container: containerName, + }).Stream(ctx) + if err != nil { + return "", fmt.Errorf("failed to get logs for pod %s container %s: %w", newest.Name, containerName, err) + } + defer stream.Close() + + buf, err := io.ReadAll(io.LimitReader(stream, maxLogBytes)) + if err != nil { + return "", fmt.Errorf("failed to read logs: %w", err) + } + return string(buf), nil +} diff --git a/internal/worker/k8s_test.go b/internal/worker/k8s_test.go new file mode 100644 index 0000000..e275460 --- /dev/null +++ b/internal/worker/k8s_test.go @@ -0,0 +1,409 @@ +package worker + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateNamespace(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + labels := map[string]string{"app": "test"} + + err := createNamespace(context.Background(), clientset, "test-ns", labels) + require.NoError(t, err) + + ns, err := clientset.CoreV1().Namespaces().Get(context.Background(), "test-ns", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "test-ns", ns.Name) + }) + + t.Run("already exists", func(t *testing.T) { + clientset := fake.NewSimpleClientset(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-ns"}, + }) + + err := createNamespace(context.Background(), clientset, "test-ns", nil) + assert.NoError(t, err) + }) + + t.Run("labels applied", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + labels := map[string]string{ + labelManagedBy: managedByValue, + labelRunID: "run-123", + } + + err := createNamespace(context.Background(), clientset, "labeled-ns", labels) + require.NoError(t, err) + + ns, err := clientset.CoreV1().Namespaces().Get(context.Background(), "labeled-ns", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, managedByValue, ns.Labels[labelManagedBy]) + assert.Equal(t, "run-123", ns.Labels[labelRunID]) + }) +} + +func TestDeleteNamespace(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "del-ns"}, + }) + + err := deleteNamespace(context.Background(), clientset, "del-ns") + require.NoError(t, err) + + _, err = clientset.CoreV1().Namespaces().Get(context.Background(), "del-ns", metav1.GetOptions{}) + assert.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run("not found is not an error", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := deleteNamespace(context.Background(), clientset, "nonexistent") + assert.NoError(t, err) + }) +} + +func TestCreateDenyAllNetworkPolicy(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := createDenyAllNetworkPolicy(context.Background(), clientset, "test-ns") + require.NoError(t, err) + + policy, err := clientset.NetworkingV1().NetworkPolicies("test-ns").Get(context.Background(), "deny-all", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "deny-all", policy.Name) + assert.Len(t, policy.Spec.PolicyTypes, 2) + require.Len(t, policy.Spec.Egress, 1) + assert.Len(t, policy.Spec.Egress[0].Ports, 2) + }) + + t.Run("already exists", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := createDenyAllNetworkPolicy(context.Background(), clientset, "test-ns") + require.NoError(t, err) + + err = createDenyAllNetworkPolicy(context.Background(), clientset, "test-ns") + assert.NoError(t, err) + }) +} + +func TestCreateDummyJob(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + labels := map[string]string{"app": "test"} + + job, err := createDummyJob(context.Background(), clientset, "ns", "test-job", "run-123", labels, 300) + require.NoError(t, err) + require.NotNil(t, job) + + assert.Equal(t, "test-job", job.Name) + assert.Equal(t, "ns", job.Namespace) + assert.Equal(t, labels, job.Labels) + + containers := job.Spec.Template.Spec.Containers + require.Len(t, containers, 1) + assert.Equal(t, defaultDummyImage, containers[0].Image) + assert.Equal(t, int32(0), *job.Spec.BackoffLimit) + assert.Equal(t, corev1.RestartPolicyNever, job.Spec.Template.Spec.RestartPolicy) + assert.Contains(t, containers[0].Command[2], "$RUN_ID") + + require.Len(t, containers[0].Env, 1) + assert.Equal(t, "RUN_ID", containers[0].Env[0].Name) + assert.Equal(t, "run-123", containers[0].Env[0].Value) + + assert.NotNil(t, containers[0].Resources.Limits) + assert.NotNil(t, containers[0].Resources.Requests) + }) + + t.Run("ttl set when positive", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + job, err := createDummyJob(context.Background(), clientset, "ns", "job-ttl", "run-1", nil, 300) + require.NoError(t, err) + require.NotNil(t, job.Spec.TTLSecondsAfterFinished) + assert.Equal(t, int32(300), *job.Spec.TTLSecondsAfterFinished) + }) + + t.Run("ttl nil when zero", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + job, err := createDummyJob(context.Background(), clientset, "ns", "job-nottl", "run-2", nil, 0) + require.NoError(t, err) + assert.Nil(t, job.Spec.TTLSecondsAfterFinished) + }) + + t.Run("already exists returns existing", func(t *testing.T) { + existing := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-job", + Namespace: "ns", + }, + } + clientset := fake.NewSimpleClientset(existing) + + job, err := createDummyJob(context.Background(), clientset, "ns", "existing-job", "run-3", nil, 0) + require.NoError(t, err) + require.NotNil(t, job) + assert.Equal(t, "existing-job", job.Name) + }) +} + +func TestWaitForJob(t *testing.T) { + t.Run("succeeded", func(t *testing.T) { + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "ok-job", Namespace: "ns"}, + Status: batchv1.JobStatus{Succeeded: 1}, + } + clientset := fake.NewSimpleClientset(job) + + passed, err := waitForJob(context.Background(), clientset, "ns", "ok-job", 5*time.Second, 50*time.Millisecond) + require.NoError(t, err) + assert.True(t, passed) + }) + + t.Run("failed", func(t *testing.T) { + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "fail-job", Namespace: "ns"}, + Status: batchv1.JobStatus{Failed: 1}, + } + clientset := fake.NewSimpleClientset(job) + + passed, err := waitForJob(context.Background(), clientset, "ns", "fail-job", 5*time.Second, 50*time.Millisecond) + require.NoError(t, err) + assert.False(t, passed) + }) + + t.Run("timeout", func(t *testing.T) { + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "slow-job", Namespace: "ns"}, + Status: batchv1.JobStatus{}, + } + clientset := fake.NewSimpleClientset(job) + + _, err := waitForJob(context.Background(), clientset, "ns", "slow-job", 150*time.Millisecond, 50*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "timeout") + }) + + t.Run("context cancelled", func(t *testing.T) { + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "cancel-job", Namespace: "ns"}, + } + clientset := fake.NewSimpleClientset(job) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := waitForJob(ctx, clientset, "ns", "cancel-job", 5*time.Second, 50*time.Millisecond) + require.Error(t, err) + assert.ErrorIs(t, err, context.Canceled) + }) +} + +func TestFetchJobLogs(t *testing.T) { + t.Run("no pods found", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + _, err := fetchJobLogs(context.Background(), clientset, "ns", "no-pods-job", 1, 10*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "no pods found") + }) + + t.Run("retry exhaustion", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + _, err := fetchJobLogs(context.Background(), clientset, "ns", "no-pods-job", 3, 10*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "3 attempts") + }) +} + +func TestCreateIntraNamespaceNetworkPolicy(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + require.NoError(t, err) + + policy, err := clientset.NetworkingV1().NetworkPolicies("test-ns").Get(context.Background(), "allow-intra-namespace", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "allow-intra-namespace", policy.Name) + assert.Len(t, policy.Spec.PolicyTypes, 2) + require.Len(t, policy.Spec.Ingress, 1) + require.Len(t, policy.Spec.Egress, 3) + }) + + t.Run("already exists", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + require.NoError(t, err) + + err = createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + assert.NoError(t, err) + }) +} + +func TestApplyDeployment(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test-dep", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "c", Image: "img"}}}, + }, + }, + } + + err := applyDeployment(context.Background(), clientset, dep) + require.NoError(t, err) + + got, err := clientset.AppsV1().Deployments("ns").Get(context.Background(), "test-dep", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "test-dep", got.Name) + }) + + t.Run("already exists", func(t *testing.T) { + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test-dep", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "c", Image: "img"}}}, + }, + }, + } + clientset := fake.NewSimpleClientset(dep) + + err := applyDeployment(context.Background(), clientset, dep) + assert.NoError(t, err) + }) +} + +func TestApplyService(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "ns"}, + Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 80}}}, + } + + err := applyService(context.Background(), clientset, svc) + require.NoError(t, err) + + got, err := clientset.CoreV1().Services("ns").Get(context.Background(), "test-svc", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "test-svc", got.Name) + }) +} + +func TestApplyConfigMap(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "ns"}, + Data: map[string]string{"key": "val"}, + } + + err := applyConfigMap(context.Background(), clientset, cm) + require.NoError(t, err) + + got, err := clientset.CoreV1().ConfigMaps("ns").Get(context.Background(), "test-cm", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "val", got.Data["key"]) + }) +} + +func TestApplyJob(t *testing.T) { + t.Run("success", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "test-job", Namespace: "ns"}, + } + + created, err := applyJob(context.Background(), clientset, job) + require.NoError(t, err) + assert.Equal(t, "test-job", created.Name) + }) + + t.Run("already exists returns existing", func(t *testing.T) { + existing := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "test-job", Namespace: "ns"}, + } + clientset := fake.NewSimpleClientset(existing) + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "test-job", Namespace: "ns"}, + } + got, err := applyJob(context.Background(), clientset, job) + require.NoError(t, err) + assert.Equal(t, "test-job", got.Name) + }) +} + +func TestWaitForDeploymentReady(t *testing.T) { + t.Run("ready immediately", func(t *testing.T) { + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "ready-dep", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + } + clientset := fake.NewSimpleClientset(dep) + + err := waitForDeploymentReady(context.Background(), clientset, "ns", "ready-dep", 5*time.Second, 50*time.Millisecond) + assert.NoError(t, err) + }) + + t.Run("timeout", func(t *testing.T) { + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "slow-dep", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 0}, + } + clientset := fake.NewSimpleClientset(dep) + + err := waitForDeploymentReady(context.Background(), clientset, "ns", "slow-dep", 150*time.Millisecond, 50*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "timeout") + }) + + t.Run("context cancelled", func(t *testing.T) { + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "cancel-dep", Namespace: "ns"}, + } + clientset := fake.NewSimpleClientset(dep) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := waitForDeploymentReady(ctx, clientset, "ns", "cancel-dep", 5*time.Second, 50*time.Millisecond) + require.Error(t, err) + assert.ErrorIs(t, err, context.Canceled) + }) +} + +func TestFetchJobLogsByContainer(t *testing.T) { + t.Run("no pods found", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + _, err := fetchJobLogsByContainer(context.Background(), clientset, "ns", "no-pods-job", "testrunner", 1, 10*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "no pods found") + }) +} diff --git a/internal/worker/manifests.go b/internal/worker/manifests.go new file mode 100644 index 0000000..da34fd5 --- /dev/null +++ b/internal/worker/manifests.go @@ -0,0 +1,475 @@ +package worker + +import ( + "encoding/json" + "fmt" + + "github.com/vultisig/plugin-tests/config" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +type verifierJSON struct { + Server struct { + Host string `json:"host"` + Port int `json:"port"` + JWTSecret string `json:"jwt_secret"` + } `json:"server"` + Database struct { + DSN string `json:"dsn"` + } `json:"database"` + Redis struct { + Host string `json:"host"` + Port string `json:"port"` + } `json:"redis"` + BlockStorage struct { + Host string `json:"host"` + Region string `json:"region"` + AccessKey string `json:"access_key"` + Secret string `json:"secret"` + Bucket string `json:"bucket"` + } `json:"block_storage"` + EncryptionSecret string `json:"encryption_secret"` + Auth struct { + Enabled bool `json:"enabled"` + } `json:"auth"` + Fees struct { + USDCAddress string `json:"usdc_address"` + } `json:"fees"` +} + +type workerJSON struct { + VaultService struct { + Relay struct { + Server string `json:"server"` + } `json:"relay"` + LocalPartyPrefix string `json:"local_party_prefix"` + EncryptionSecret string `json:"encryption_secret"` + DoSetupMsg bool `json:"do_setup_msg"` + } `json:"vault_service"` + Database struct { + DSN string `json:"dsn"` + } `json:"database"` + Redis struct { + Host string `json:"host"` + Port string `json:"port"` + } `json:"redis"` + BlockStorage struct { + Host string `json:"host"` + Region string `json:"region"` + AccessKey string `json:"access_key"` + Secret string `json:"secret"` + Bucket string `json:"bucket"` + } `json:"block_storage"` + Fees struct { + USDCAddress string `json:"usdc_address"` + } `json:"fees"` +} + +func int32Ptr(i int32) *int32 { return &i } + +func infraPostgresObjects(ns, image string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { + selectorLabels := map[string]string{"app": "postgres"} + allLabels := mergeLabels(labels, selectorLabels) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "postgres", Namespace: ns, Labels: allLabels}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: selectorLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "postgres", + Image: image, + Env: []corev1.EnvVar{ + {Name: "POSTGRES_USER", Value: "vultisig"}, + {Name: "POSTGRES_PASSWORD", Value: "vultisig"}, + {Name: "POSTGRES_DB", Value: "vultisig-verifier"}, + }, + Ports: []corev1.ContainerPort{{ContainerPort: 5432}}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt32(5432)}, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 3, + }, + Resources: infraResources(), + }}, + }, + }, + }, + } + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "postgres", Namespace: ns, Labels: allLabels}, + Spec: corev1.ServiceSpec{ + Selector: selectorLabels, + Ports: []corev1.ServicePort{{Port: 5432, TargetPort: intstr.FromInt32(5432)}}, + }, + } + return dep, svc +} + +func infraRedisObjects(ns, image string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { + selectorLabels := map[string]string{"app": "redis"} + allLabels := mergeLabels(labels, selectorLabels) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: ns, Labels: allLabels}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: selectorLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "redis", + Image: image, + Ports: []corev1.ContainerPort{{ContainerPort: 6379}}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt32(6379)}, + }, + InitialDelaySeconds: 3, + PeriodSeconds: 3, + }, + Resources: infraResources(), + }}, + }, + }, + }, + } + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: ns, Labels: allLabels}, + Spec: corev1.ServiceSpec{ + Selector: selectorLabels, + Ports: []corev1.ServicePort{{Port: 6379, TargetPort: intstr.FromInt32(6379)}}, + }, + } + return dep, svc +} + +func infraMinioObjects(ns, image string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { + selectorLabels := map[string]string{"app": "minio"} + allLabels := mergeLabels(labels, selectorLabels) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "minio", Namespace: ns, Labels: allLabels}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: selectorLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "minio", + Image: image, + Command: []string{"minio", "server", "/data"}, + Env: []corev1.EnvVar{ + {Name: "MINIO_ROOT_USER", Value: "minioadmin"}, + {Name: "MINIO_ROOT_PASSWORD", Value: "minioadmin"}, + }, + Ports: []corev1.ContainerPort{{ContainerPort: 9000}}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/minio/health/live", + Port: intstr.FromInt32(9000), + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 3, + }, + Resources: infraResources(), + }}, + }, + }, + }, + } + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "minio", Namespace: ns, Labels: allLabels}, + Spec: corev1.ServiceSpec{ + Selector: selectorLabels, + Ports: []corev1.ServicePort{{Port: 9000, TargetPort: intstr.FromInt32(9000)}}, + }, + } + return dep, svc +} + +func verifierConfigMap(ns string, cfg config.K8sJobConfig) (*corev1.ConfigMap, error) { + var v verifierJSON + v.Server.Host = "0.0.0.0" + v.Server.Port = 8080 + v.Server.JWTSecret = cfg.JWTSecret + v.Database.DSN = "postgres://vultisig:vultisig@postgres:5432/vultisig-verifier?sslmode=disable" + v.Redis.Host = "redis" + v.Redis.Port = "6379" + v.BlockStorage.Host = "http://minio:9000" + v.BlockStorage.Region = "us-east-1" + v.BlockStorage.AccessKey = "minioadmin" + v.BlockStorage.Secret = "minioadmin" + v.BlockStorage.Bucket = "vultisig-verifier" + v.EncryptionSecret = cfg.EncryptionSecret + v.Auth.Enabled = true + v.Fees.USDCAddress = "0x0000000000000000000000000000000000000000" + + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal verifier config: %w", err) + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "verifier-config", Namespace: ns}, + Data: map[string]string{"config.json": string(data)}, + }, nil +} + +func workerConfigMap(ns string, cfg config.K8sJobConfig) (*corev1.ConfigMap, error) { + var w workerJSON + w.VaultService.Relay.Server = "https://api.vultisig.com/router" + w.VaultService.LocalPartyPrefix = "verifier" + w.VaultService.EncryptionSecret = cfg.EncryptionSecret + w.VaultService.DoSetupMsg = false + w.Database.DSN = "postgres://vultisig:vultisig@postgres:5432/vultisig-verifier?sslmode=disable" + w.Redis.Host = "redis" + w.Redis.Port = "6379" + w.BlockStorage.Host = "http://minio:9000" + w.BlockStorage.Region = "us-east-1" + w.BlockStorage.AccessKey = "minioadmin" + w.BlockStorage.Secret = "minioadmin" + w.BlockStorage.Bucket = "vultisig-verifier" + w.Fees.USDCAddress = "0x0000000000000000000000000000000000000000" + + data, err := json.MarshalIndent(w, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal worker config: %w", err) + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "worker-config", Namespace: ns}, + Data: map[string]string{"config.json": string(data)}, + }, nil +} + +func verifierDeploymentObjects(ns, image, pullSecret string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { + selectorLabels := map[string]string{"app": "verifier"} + allLabels := mergeLabels(labels, selectorLabels) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "verifier", Namespace: ns, Labels: allLabels}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: selectorLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "verifier", + Image: image, + Ports: []corev1.ContainerPort{{ContainerPort: 8080}}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/plugins", + Port: intstr.FromInt32(8080), + }, + }, + InitialDelaySeconds: 10, + PeriodSeconds: 5, + }, + Resources: verifierResources(), + VolumeMounts: []corev1.VolumeMount{{ + Name: "config", + MountPath: "/app/config.json", + SubPath: "config.json", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "verifier-config"}, + }, + }, + }}, + }, + }, + }, + } + + if pullSecret != "" { + dep.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: pullSecret}} + } + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "verifier", Namespace: ns, Labels: allLabels}, + Spec: corev1.ServiceSpec{ + Selector: selectorLabels, + Ports: []corev1.ServicePort{{Port: 8080, TargetPort: intstr.FromInt32(8080)}}, + }, + } + return dep, svc +} + +func verifierWorkerDeployment(ns, image, pullSecret string, labels map[string]string) *appsv1.Deployment { + selectorLabels := map[string]string{"app": "verifier-worker"} + allLabels := mergeLabels(labels, selectorLabels) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "verifier-worker", Namespace: ns, Labels: allLabels}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: selectorLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "worker", + Image: image, + Resources: verifierResources(), + VolumeMounts: []corev1.VolumeMount{{ + Name: "config", + MountPath: "/app/config.json", + SubPath: "config.json", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "worker-config"}, + }, + }, + }}, + }, + }, + }, + } + + if pullSecret != "" { + dep.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: pullSecret}} + } + + return dep +} + +func seederJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { + return buildTestJob("seeder", ns, image, pullSecret, labels, []string{"seed"}, envVars, ttlSeconds) +} + +func testJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { + return buildTestJob("test", ns, image, pullSecret, labels, []string{"test"}, envVars, ttlSeconds) +} + +func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, args []string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { + var backoffLimit int32 + var ttlPtr *int32 + if ttlSeconds > 0 { + ttlPtr = &ttlSeconds + } + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: &backoffLimit, + TTLSecondsAfterFinished: ttlPtr, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: labels}, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{ + Name: "testrunner", + Image: image, + Args: args, + Env: envVars, + Resources: jobResources(), + }}, + }, + }, + }, + } + + if pullSecret != "" { + job.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: pullSecret}} + } + + return job +} + +func testrunnerEnvVars(cfg config.K8sJobConfig) []corev1.EnvVar { + return []corev1.EnvVar{ + {Name: "POSTGRES_DSN", Value: "postgres://vultisig:vultisig@postgres:5432/vultisig-verifier?sslmode=disable"}, + {Name: "MINIO_ENDPOINT", Value: "http://minio:9000"}, + {Name: "MINIO_ACCESS_KEY", Value: "minioadmin"}, + {Name: "MINIO_SECRET_KEY", Value: "minioadmin"}, + {Name: "MINIO_BUCKET", Value: "vultisig-verifier"}, + {Name: "ENCRYPTION_SECRET", Value: cfg.EncryptionSecret}, + {Name: "VERIFIER_URL", Value: "http://verifier:8080"}, + {Name: "JWT_SECRET", Value: cfg.JWTSecret}, + {Name: "PLUGIN_ENDPOINT", Value: cfg.PluginEndpoint}, + } +} + +func infraResources() corev1.ResourceRequirements { + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + } +} + +func verifierResources() corev1.ResourceRequirements { + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + } +} + +func jobResources() corev1.ResourceRequirements { + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + } +} + +func mergeLabels(base, extra map[string]string) map[string]string { + merged := make(map[string]string, len(base)+len(extra)) + for k, v := range base { + merged[k] = v + } + for k, v := range extra { + merged[k] = v + } + return merged +} diff --git a/internal/worker/manifests_test.go b/internal/worker/manifests_test.go new file mode 100644 index 0000000..dde58c1 --- /dev/null +++ b/internal/worker/manifests_test.go @@ -0,0 +1,199 @@ +package worker + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vultisig/plugin-tests/config" +) + +var testK8sCfg = config.K8sJobConfig{ + EncryptionSecret: "test-secret", + JWTSecret: "jwt-test-secret", + PluginEndpoint: "https://plugin.example.com", +} + +func TestInfraPostgresObjects(t *testing.T) { + labels := map[string]string{"run": "123"} + dep, svc := infraPostgresObjects("test-ns", "postgres:15", labels) + + assert.Equal(t, "postgres", dep.Name) + assert.Equal(t, "test-ns", dep.Namespace) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + assert.Equal(t, "postgres:15", dep.Spec.Template.Spec.Containers[0].Image) + assert.NotNil(t, dep.Spec.Template.Spec.Containers[0].ReadinessProbe) + + assert.Equal(t, "postgres", svc.Name) + require.Len(t, svc.Spec.Ports, 1) + assert.Equal(t, int32(5432), svc.Spec.Ports[0].Port) +} + +func TestInfraRedisObjects(t *testing.T) { + dep, svc := infraRedisObjects("test-ns", "redis:7", nil) + + assert.Equal(t, "redis", dep.Name) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + assert.Equal(t, "redis:7", dep.Spec.Template.Spec.Containers[0].Image) + + assert.Equal(t, "redis", svc.Name) + require.Len(t, svc.Spec.Ports, 1) + assert.Equal(t, int32(6379), svc.Spec.Ports[0].Port) +} + +func TestInfraMinioObjects(t *testing.T) { + dep, svc := infraMinioObjects("test-ns", "minio/minio:latest", nil) + + assert.Equal(t, "minio", dep.Name) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + c := dep.Spec.Template.Spec.Containers[0] + assert.Equal(t, "minio/minio:latest", c.Image) + assert.Equal(t, []string{"minio", "server", "/data"}, c.Command) + require.NotNil(t, c.ReadinessProbe) + assert.Equal(t, "/minio/health/live", c.ReadinessProbe.HTTPGet.Path) + + assert.Equal(t, "minio", svc.Name) + require.Len(t, svc.Spec.Ports, 1) + assert.Equal(t, int32(9000), svc.Spec.Ports[0].Port) +} + +func TestVerifierConfigMap(t *testing.T) { + cm, err := verifierConfigMap("test-ns", testK8sCfg) + require.NoError(t, err) + + assert.Equal(t, "verifier-config", cm.Name) + assert.Equal(t, "test-ns", cm.Namespace) + + raw := cm.Data["config.json"] + require.NotEmpty(t, raw) + + var parsed verifierJSON + err = json.Unmarshal([]byte(raw), &parsed) + require.NoError(t, err) + + assert.Equal(t, "0.0.0.0", parsed.Server.Host) + assert.Equal(t, 8080, parsed.Server.Port) + assert.Equal(t, "jwt-test-secret", parsed.Server.JWTSecret) + assert.Equal(t, "redis", parsed.Redis.Host) + assert.Equal(t, "http://minio:9000", parsed.BlockStorage.Host) + assert.Equal(t, "test-secret", parsed.EncryptionSecret) + assert.True(t, parsed.Auth.Enabled) +} + +func TestWorkerConfigMap(t *testing.T) { + cm, err := workerConfigMap("test-ns", testK8sCfg) + require.NoError(t, err) + + assert.Equal(t, "worker-config", cm.Name) + + raw := cm.Data["config.json"] + require.NotEmpty(t, raw) + + var parsed workerJSON + err = json.Unmarshal([]byte(raw), &parsed) + require.NoError(t, err) + + assert.Equal(t, "https://api.vultisig.com/router", parsed.VaultService.Relay.Server) + assert.Equal(t, "verifier", parsed.VaultService.LocalPartyPrefix) + assert.Equal(t, "test-secret", parsed.VaultService.EncryptionSecret) + assert.False(t, parsed.VaultService.DoSetupMsg) +} + +func TestVerifierDeploymentObjects(t *testing.T) { + labels := map[string]string{"run": "123"} + + t.Run("with pull secret", func(t *testing.T) { + dep, svc := verifierDeploymentObjects("ns", "verifier:v1", "my-secret", labels) + + assert.Equal(t, "verifier", dep.Name) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + c := dep.Spec.Template.Spec.Containers[0] + assert.Equal(t, "verifier:v1", c.Image) + require.NotNil(t, c.ReadinessProbe) + assert.Equal(t, "/plugins", c.ReadinessProbe.HTTPGet.Path) + require.Len(t, c.VolumeMounts, 1) + assert.Equal(t, "/app/config.json", c.VolumeMounts[0].MountPath) + assert.Equal(t, "config.json", c.VolumeMounts[0].SubPath) + require.Len(t, dep.Spec.Template.Spec.ImagePullSecrets, 1) + assert.Equal(t, "my-secret", dep.Spec.Template.Spec.ImagePullSecrets[0].Name) + + assert.Equal(t, "verifier", svc.Name) + require.Len(t, svc.Spec.Ports, 1) + assert.Equal(t, int32(8080), svc.Spec.Ports[0].Port) + }) + + t.Run("without pull secret", func(t *testing.T) { + dep, _ := verifierDeploymentObjects("ns", "verifier:v1", "", labels) + assert.Empty(t, dep.Spec.Template.Spec.ImagePullSecrets) + }) +} + +func TestVerifierWorkerDeployment(t *testing.T) { + dep := verifierWorkerDeployment("ns", "worker:v1", "my-secret", nil) + + assert.Equal(t, "verifier-worker", dep.Name) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + c := dep.Spec.Template.Spec.Containers[0] + assert.Equal(t, "worker:v1", c.Image) + require.Len(t, c.VolumeMounts, 1) + assert.Equal(t, "/app/config.json", c.VolumeMounts[0].MountPath) +} + +func TestSeederJob(t *testing.T) { + envVars := testrunnerEnvVars(testK8sCfg) + job := seederJob("ns", "testrunner:v1", "", nil, envVars, 300) + + assert.Equal(t, "seeder", job.Name) + assert.Equal(t, "ns", job.Namespace) + require.Len(t, job.Spec.Template.Spec.Containers, 1) + c := job.Spec.Template.Spec.Containers[0] + assert.Equal(t, "testrunner", c.Name) + assert.Equal(t, "testrunner:v1", c.Image) + assert.Equal(t, []string{"seed"}, c.Args) + assert.NotEmpty(t, c.Env) + assert.Equal(t, int32(0), *job.Spec.BackoffLimit) + require.NotNil(t, job.Spec.TTLSecondsAfterFinished) + assert.Equal(t, int32(300), *job.Spec.TTLSecondsAfterFinished) +} + +func TestTestJob(t *testing.T) { + envVars := testrunnerEnvVars(testK8sCfg) + job := testJob("ns", "testrunner:v1", "my-secret", nil, envVars, 0) + + assert.Equal(t, "test", job.Name) + require.Len(t, job.Spec.Template.Spec.Containers, 1) + assert.Equal(t, []string{"test"}, job.Spec.Template.Spec.Containers[0].Args) + assert.Nil(t, job.Spec.TTLSecondsAfterFinished) + require.Len(t, job.Spec.Template.Spec.ImagePullSecrets, 1) +} + +func TestTestrunnerEnvVars(t *testing.T) { + vars := testrunnerEnvVars(testK8sCfg) + + envMap := make(map[string]string) + for _, v := range vars { + envMap[v.Name] = v.Value + } + + assert.Contains(t, envMap, "POSTGRES_DSN") + assert.Contains(t, envMap, "MINIO_ENDPOINT") + assert.Contains(t, envMap, "ENCRYPTION_SECRET") + assert.Equal(t, "test-secret", envMap["ENCRYPTION_SECRET"]) + assert.Equal(t, "jwt-test-secret", envMap["JWT_SECRET"]) + assert.Equal(t, "https://plugin.example.com", envMap["PLUGIN_ENDPOINT"]) + assert.Equal(t, "http://verifier:8080", envMap["VERIFIER_URL"]) +} + +func TestMergeLabels(t *testing.T) { + base := map[string]string{"a": "1", "b": "2"} + extra := map[string]string{"c": "3", "a": "override"} + + merged := mergeLabels(base, extra) + + assert.Equal(t, "override", merged["a"]) + assert.Equal(t, "2", merged["b"]) + assert.Equal(t, "3", merged["c"]) + assert.Equal(t, "1", base["a"]) +} diff --git a/internal/worker/naming.go b/internal/worker/naming.go new file mode 100644 index 0000000..d627567 --- /dev/null +++ b/internal/worker/naming.go @@ -0,0 +1,90 @@ +package worker + +import ( + "strings" +) + +const ( + maxDNSLabelLen = 63 + runIDPrefixLen = 12 + + labelManagedBy = "app.kubernetes.io/managed-by" + labelRunID = "plugin-tests/run-id" + labelPluginID = "plugin-tests/plugin-id" + labelKind = "plugin-tests/kind" + + managedByValue = "plugin-tests" + kindDummy = "dummy" + kindIntegration = "integration" + + labelComponent = "plugin-tests/component" +) + +func namespaceName(runID string) string { + return dnsLabel("plugin-test-" + runIDPrefix(runID)) +} + +func namespaceNameWithPlugin(runID, pluginID string) string { + plugin := dnsLabel(pluginID) + maxPluginLen := maxDNSLabelLen - len("plugin-test--") - runIDPrefixLen + if len(plugin) > maxPluginLen { + plugin = plugin[:maxPluginLen] + } + plugin = strings.TrimRight(plugin, "-") + return dnsLabel("plugin-test-" + plugin + "-" + runIDPrefix(runID)) +} + +func jobName(runID string) string { + return dnsLabel("dummy-" + runIDPrefix(runID)) +} + +func seederJobName(runID string) string { + return dnsLabel("seeder-" + runIDPrefix(runID)) +} + +func testJobName(runID string) string { + return dnsLabel("test-" + runIDPrefix(runID)) +} + +func runLabels(runID, pluginID, kind string) map[string]string { + return map[string]string{ + labelManagedBy: managedByValue, + labelRunID: runID, + labelPluginID: dnsLabel(pluginID), + labelKind: kind, + } +} + +func runIDPrefix(id string) string { + clean := strings.ReplaceAll(id, "-", "") + clean = strings.ToLower(clean) + if clean == "" { + return "unknown" + } + if len(clean) > runIDPrefixLen { + return clean[:runIDPrefixLen] + } + return clean +} + +func dnsLabel(s string) string { + s = strings.ToLower(s) + + var b strings.Builder + for _, c := range s { + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { + b.WriteRune(c) + } + } + result := b.String() + + if len(result) > maxDNSLabelLen { + result = result[:maxDNSLabelLen] + } + + result = strings.Trim(result, "-") + if result == "" { + return "x" + } + return result +} diff --git a/internal/worker/naming_test.go b/internal/worker/naming_test.go new file mode 100644 index 0000000..e9f1d97 --- /dev/null +++ b/internal/worker/naming_test.go @@ -0,0 +1,139 @@ +package worker + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDnsLabel(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"simple lowercase", "hello", "hello"}, + {"mixed case with dash", "Hello-World", "hello-world"}, + {"strips invalid chars", "ABC_DEF!@#123", "abcdef123"}, + {"empty string", "", "x"}, + {"all dashes", "---", "x"}, + {"leading trailing dashes", "-abc-", "abc"}, + {"truncates to 63", strings.Repeat("a", 100), strings.Repeat("a", 63)}, + {"truncate then trim trailing dash", strings.Repeat("a", 62) + "-z", strings.Repeat("a", 62)}, + {"trailing dash after truncate", strings.Repeat("a", 63) + "-", strings.Repeat("a", 63)}, + {"unicode stripped", "café", "caf"}, + {"numbers allowed", "test-123-run", "test-123-run"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := dnsLabel(tt.input) + assert.Equal(t, tt.expected, result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) + }) + } +} + +func TestRunIDPrefix(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"standard uuid", "550e8400-e29b-41d4-a716-446655440000", "550e8400e29b"}, + {"empty string", "", "unknown"}, + {"short string", "short", "short"}, + {"exact 12 chars", "abcdefghijkl", "abcdefghijkl"}, + {"over 12 chars", "abcdefghijklmnop", "abcdefghijkl"}, + {"uppercase", "ABC-DEF", "abcdef"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := runIDPrefix(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNamespaceName(t *testing.T) { + tests := []struct { + name string + runID string + expected string + }{ + {"standard uuid", "550e8400-e29b-41d4-a716-446655440000", "plugin-test-550e8400e29b"}, + {"empty run id", "", "plugin-test-unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := namespaceName(tt.runID) + assert.Equal(t, tt.expected, result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) + }) + } +} + +func TestJobName(t *testing.T) { + tests := []struct { + name string + runID string + expected string + }{ + {"standard uuid", "550e8400-e29b-41d4-a716-446655440000", "dummy-550e8400e29b"}, + {"empty run id", "", "dummy-unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := jobName(tt.runID) + assert.Equal(t, tt.expected, result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) + }) + } +} + +func TestNamespaceNameWithPlugin(t *testing.T) { + tests := []struct { + name string + runID string + pluginID string + expected string + }{ + {"standard", "550e8400-e29b-41d4-a716-446655440000", "vultisig-dca-0000", "plugin-test-vultisig-dca-0000-550e8400e29b"}, + {"long plugin id", "550e8400-e29b-41d4-a716-446655440000", "vultisig-recurring-sends-really-long-name-0000", "plugin-test-vultisig-recurring-sends-really-long-n-550e8400e29b"}, + {"empty run id", "", "vultisig-dca-0000", "plugin-test-vultisig-dca-0000-unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := namespaceNameWithPlugin(tt.runID, tt.pluginID) + assert.Equal(t, tt.expected, result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) + }) + } +} + +func TestSeederJobName(t *testing.T) { + result := seederJobName("550e8400-e29b-41d4-a716-446655440000") + assert.Equal(t, "seeder-550e8400e29b", result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) +} + +func TestTestJobName(t *testing.T) { + result := testJobName("550e8400-e29b-41d4-a716-446655440000") + assert.Equal(t, "test-550e8400e29b", result) + assert.LessOrEqual(t, len(result), maxDNSLabelLen) +} + +func TestRunLabels(t *testing.T) { + labels := runLabels("some-run-id", "my-plugin", "dummy") + + assert.Len(t, labels, 4) + assert.Equal(t, managedByValue, labels[labelManagedBy]) + assert.Equal(t, "some-run-id", labels[labelRunID]) + assert.Equal(t, "my-plugin", labels[labelPluginID]) + assert.Equal(t, "dummy", labels[labelKind]) +} diff --git a/internal/worker/runner.go b/internal/worker/runner.go new file mode 100644 index 0000000..3735cc0 --- /dev/null +++ b/internal/worker/runner.go @@ -0,0 +1,282 @@ +package worker + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + + "github.com/vultisig/plugin-tests/config" +) + +const ( + defaultPostgresImage = "postgres:15" + defaultRedisImage = "redis:7" + defaultMinioImage = "minio/minio:latest" +) + +type Runner struct { + k8s kubernetes.Interface + cfg config.K8sJobConfig + logger *logrus.Entry +} + +type RunResult struct { + Passed bool + SeederLogs string + TestLogs string + ErrorMsg string +} + +func NewRunner(k8s kubernetes.Interface, cfg config.K8sJobConfig, logger *logrus.Entry) *Runner { + return &Runner{ + k8s: k8s, + cfg: cfg, + logger: logger, + } +} + +func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, labels map[string]string) *RunResult { + result := &RunResult{} + + err := r.deployNetworkPolicy(ctx, namespace) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + err = r.deployInfra(ctx, namespace, labels) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + err = r.waitForInfra(ctx, namespace) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + err = r.deployConfigMaps(ctx, namespace) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + err = r.deployVerifier(ctx, namespace, labels) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + err = r.waitForVerifier(ctx, namespace) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + + result.SeederLogs, err = r.runSeederJob(ctx, namespace, runID, labels) + if err != nil { + result.ErrorMsg = fmt.Sprintf("seeder failed: %s", err.Error()) + return result + } + + testLogs, testPassed, err := r.runTestJob(ctx, namespace, runID, labels) + result.TestLogs = testLogs + if err != nil { + result.ErrorMsg = fmt.Sprintf("test job failed: %s", err.Error()) + return result + } + + result.Passed = testPassed + return result +} + +func (r *Runner) deployNetworkPolicy(ctx context.Context, ns string) error { + r.logger.Info("creating network policy") + return createIntraNamespaceNetworkPolicy(ctx, r.k8s, ns) +} + +func (r *Runner) deployInfra(ctx context.Context, ns string, labels map[string]string) error { + pgImage := r.imageOrDefault(r.cfg.PostgresImage, defaultPostgresImage) + redisImage := r.imageOrDefault(r.cfg.RedisImage, defaultRedisImage) + minioImage := r.imageOrDefault(r.cfg.MinioImage, defaultMinioImage) + + pgDep, pgSvc := infraPostgresObjects(ns, pgImage, labels) + err := applyDeployment(ctx, r.k8s, pgDep) + if err != nil { + return fmt.Errorf("deploy postgres: %w", err) + } + err = applyService(ctx, r.k8s, pgSvc) + if err != nil { + return fmt.Errorf("deploy postgres service: %w", err) + } + r.logger.Info("postgres deployed") + + redisDep, redisSvc := infraRedisObjects(ns, redisImage, labels) + err = applyDeployment(ctx, r.k8s, redisDep) + if err != nil { + return fmt.Errorf("deploy redis: %w", err) + } + err = applyService(ctx, r.k8s, redisSvc) + if err != nil { + return fmt.Errorf("deploy redis service: %w", err) + } + r.logger.Info("redis deployed") + + minioDep, minioSvc := infraMinioObjects(ns, minioImage, labels) + err = applyDeployment(ctx, r.k8s, minioDep) + if err != nil { + return fmt.Errorf("deploy minio: %w", err) + } + err = applyService(ctx, r.k8s, minioSvc) + if err != nil { + return fmt.Errorf("deploy minio service: %w", err) + } + r.logger.Info("minio deployed") + + return nil +} + +func (r *Runner) waitForInfra(ctx context.Context, ns string) error { + for _, name := range []string{"postgres", "redis", "minio"} { + r.logger.WithField("deployment", name).Info("waiting for readiness") + err := waitForDeploymentReady(ctx, r.k8s, ns, name, r.timeout(5*time.Minute), r.pollInterval()) + if err != nil { + return fmt.Errorf("wait for %s: %w", name, err) + } + } + r.logger.Info("infra ready") + return nil +} + +func (r *Runner) deployConfigMaps(ctx context.Context, ns string) error { + vcm, err := verifierConfigMap(ns, r.cfg) + if err != nil { + return fmt.Errorf("build verifier configmap: %w", err) + } + err = applyConfigMap(ctx, r.k8s, vcm) + if err != nil { + return fmt.Errorf("apply verifier configmap: %w", err) + } + + wcm, err := workerConfigMap(ns, r.cfg) + if err != nil { + return fmt.Errorf("build worker configmap: %w", err) + } + err = applyConfigMap(ctx, r.k8s, wcm) + if err != nil { + return fmt.Errorf("apply worker configmap: %w", err) + } + r.logger.Info("configmaps created") + return nil +} + +func (r *Runner) deployVerifier(ctx context.Context, ns string, labels map[string]string) error { + vDep, vSvc := verifierDeploymentObjects(ns, r.cfg.VerifierImage, r.cfg.ImagePullSecret, labels) + err := applyDeployment(ctx, r.k8s, vDep) + if err != nil { + return fmt.Errorf("deploy verifier: %w", err) + } + err = applyService(ctx, r.k8s, vSvc) + if err != nil { + return fmt.Errorf("deploy verifier service: %w", err) + } + r.logger.Info("verifier deployed") + + wDep := verifierWorkerDeployment(ns, r.cfg.VerifierWorkerImage, r.cfg.ImagePullSecret, labels) + err = applyDeployment(ctx, r.k8s, wDep) + if err != nil { + return fmt.Errorf("deploy verifier-worker: %w", err) + } + r.logger.Info("verifier-worker deployed") + + return nil +} + +func (r *Runner) waitForVerifier(ctx context.Context, ns string) error { + r.logger.Info("waiting for verifier readiness") + err := waitForDeploymentReady(ctx, r.k8s, ns, "verifier", r.timeout(5*time.Minute), r.pollInterval()) + if err != nil { + return fmt.Errorf("wait for verifier: %w", err) + } + r.logger.Info("verifier ready") + return nil +} + +func (r *Runner) runSeederJob(ctx context.Context, ns, runID string, labels map[string]string) (string, error) { + envVars := testrunnerEnvVars(r.cfg) + name := seederJobName(runID) + job := seederJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished) + job.Name = name + + r.logger.WithField("job", name).Info("running seeder") + _, err := applyJob(ctx, r.k8s, job) + if err != nil { + return "", fmt.Errorf("create seeder job: %w", err) + } + + passed, err := waitForJob(ctx, r.k8s, ns, name, r.timeout(5*time.Minute), r.pollInterval()) + if err != nil { + return "", fmt.Errorf("wait for seeder: %w", err) + } + + logs, logErr := fetchJobLogsByContainer(ctx, r.k8s, ns, name, "testrunner", 3, 2*time.Second) + if logErr != nil { + r.logger.WithError(logErr).Warn("failed to fetch seeder logs") + } + + if !passed { + return logs, fmt.Errorf("seeder job failed") + } + r.logger.Info("seeder completed") + return logs, nil +} + +func (r *Runner) runTestJob(ctx context.Context, ns, runID string, labels map[string]string) (string, bool, error) { + envVars := testrunnerEnvVars(r.cfg) + name := testJobName(runID) + job := testJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished) + job.Name = name + + r.logger.WithField("job", name).Info("running tests") + _, err := applyJob(ctx, r.k8s, job) + if err != nil { + return "", false, fmt.Errorf("create test job: %w", err) + } + + passed, err := waitForJob(ctx, r.k8s, ns, name, r.timeout(10*time.Minute), r.pollInterval()) + if err != nil { + return "", false, fmt.Errorf("wait for test: %w", err) + } + + logs, logErr := fetchJobLogsByContainer(ctx, r.k8s, ns, name, "testrunner", 3, 2*time.Second) + if logErr != nil { + r.logger.WithError(logErr).Warn("failed to fetch test logs") + } + + return logs, passed, nil +} + +func (r *Runner) imageOrDefault(image, defaultImage string) string { + if image != "" { + return image + } + return defaultImage +} + +func (r *Runner) timeout(fallback time.Duration) time.Duration { + if r.cfg.JobTimeout > 0 { + return r.cfg.JobTimeout + } + return fallback +} + +func (r *Runner) pollInterval() time.Duration { + if r.cfg.PollInterval > 0 { + return r.cfg.PollInterval + } + return 2 * time.Second +} diff --git a/internal/worker/runner_test.go b/internal/worker/runner_test.go new file mode 100644 index 0000000..3a3cc9f --- /dev/null +++ b/internal/worker/runner_test.go @@ -0,0 +1,175 @@ +package worker + +import ( + "context" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/vultisig/plugin-tests/config" +) + +func newTestRunner(clientset *fake.Clientset) *Runner { + cfg := config.K8sJobConfig{ + JobTimeout: 5 * time.Second, + PollInterval: 50 * time.Millisecond, + TTLAfterFinished: 300, + VerifierImage: "verifier:test", + VerifierWorkerImage: "worker:test", + TestImage: "testrunner:test", + EncryptionSecret: "test-secret", + JWTSecret: "jwt-secret", + PluginEndpoint: "https://plugin.example.com", + } + logger := logrus.New() + logger.SetLevel(logrus.DebugLevel) + entry := logger.WithField("test", true) + return NewRunner(clientset, cfg, entry) +} + +func TestRunner_DeployNetworkPolicy(t *testing.T) { + clientset := fake.NewSimpleClientset() + runner := newTestRunner(clientset) + + err := runner.deployNetworkPolicy(context.Background(), "test-ns") + require.NoError(t, err) + + policy, err := clientset.NetworkingV1().NetworkPolicies("test-ns").Get(context.Background(), "allow-intra-namespace", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "allow-intra-namespace", policy.Name) +} + +func TestRunner_DeployInfra(t *testing.T) { + clientset := fake.NewSimpleClientset() + runner := newTestRunner(clientset) + labels := map[string]string{"test": "true"} + + err := runner.deployInfra(context.Background(), "test-ns", labels) + require.NoError(t, err) + + for _, name := range []string{"postgres", "redis", "minio"} { + dep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), name, metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, name, dep.Name) + + svc, err := clientset.CoreV1().Services("test-ns").Get(context.Background(), name, metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, name, svc.Name) + } +} + +func TestRunner_DeployInfra_CustomImages(t *testing.T) { + clientset := fake.NewSimpleClientset() + runner := newTestRunner(clientset) + runner.cfg.PostgresImage = "postgres:16" + runner.cfg.RedisImage = "redis:8" + runner.cfg.MinioImage = "minio/minio:2024" + + err := runner.deployInfra(context.Background(), "test-ns", nil) + require.NoError(t, err) + + pgDep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), "postgres", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "postgres:16", pgDep.Spec.Template.Spec.Containers[0].Image) + + redisDep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), "redis", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "redis:8", redisDep.Spec.Template.Spec.Containers[0].Image) + + minioDep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), "minio", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "minio/minio:2024", minioDep.Spec.Template.Spec.Containers[0].Image) +} + +func TestRunner_WaitForInfra_AllReady(t *testing.T) { + clientset := fake.NewSimpleClientset( + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "postgres", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "minio", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + }, + ) + runner := newTestRunner(clientset) + + err := runner.waitForInfra(context.Background(), "ns") + assert.NoError(t, err) +} + +func TestRunner_WaitForInfra_Timeout(t *testing.T) { + clientset := fake.NewSimpleClientset( + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "postgres", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 0}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "minio", Namespace: "ns"}, + Status: appsv1.DeploymentStatus{ReadyReplicas: 1}, + }, + ) + runner := newTestRunner(clientset) + runner.cfg.JobTimeout = 200 * time.Millisecond + + err := runner.waitForInfra(context.Background(), "ns") + require.Error(t, err) + assert.Contains(t, err.Error(), "redis") +} + +func TestRunner_DeployConfigMaps(t *testing.T) { + clientset := fake.NewSimpleClientset() + runner := newTestRunner(clientset) + + err := runner.deployConfigMaps(context.Background(), "test-ns") + require.NoError(t, err) + + vcm, err := clientset.CoreV1().ConfigMaps("test-ns").Get(context.Background(), "verifier-config", metav1.GetOptions{}) + require.NoError(t, err) + assert.Contains(t, vcm.Data["config.json"], "jwt-secret") + + wcm, err := clientset.CoreV1().ConfigMaps("test-ns").Get(context.Background(), "worker-config", metav1.GetOptions{}) + require.NoError(t, err) + assert.Contains(t, wcm.Data["config.json"], "vault_service") +} + +func TestRunner_DeployVerifier(t *testing.T) { + clientset := fake.NewSimpleClientset() + runner := newTestRunner(clientset) + + err := runner.deployVerifier(context.Background(), "test-ns", nil) + require.NoError(t, err) + + vDep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), "verifier", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "verifier:test", vDep.Spec.Template.Spec.Containers[0].Image) + + wDep, err := clientset.AppsV1().Deployments("test-ns").Get(context.Background(), "verifier-worker", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "worker:test", wDep.Spec.Template.Spec.Containers[0].Image) + + svc, err := clientset.CoreV1().Services("test-ns").Get(context.Background(), "verifier", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "verifier", svc.Name) +} + +func TestRunner_ImageOrDefault(t *testing.T) { + runner := &Runner{} + + assert.Equal(t, "custom:v1", runner.imageOrDefault("custom:v1", "default:v1")) + assert.Equal(t, "default:v1", runner.imageOrDefault("", "default:v1")) +} From 0ece817a65b00c29408a781ecc1bdcc83e712ade Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:15:22 +0400 Subject: [PATCH 2/7] add artifacts handling --- cmd/server/main.go | 2 +- cmd/testrunner/main.go | 10 +- config/config.go | 2 + deploy/minikube/minio.yaml | 52 +++ deploy/minikube/namespace.yaml | 6 + deploy/minikube/postgres.yaml | 52 +++ deploy/minikube/rbac.yaml | 45 ++ deploy/minikube/redis.yaml | 45 ++ deploy/minikube/server.yaml | 70 +++ deploy/minikube/worker.yaml | 71 +++ internal/api/artifacts.go | 89 ++++ internal/api/handler.go | 32 +- internal/api/results.go | 219 +++++++++ internal/api/server.go | 31 +- internal/api/templates/detail.html | 71 +++ internal/api/templates/layout.html | 60 +++ internal/api/templates/list.html | 66 +++ .../storage/postgres/queries/test_runs.sql.go | 52 ++- internal/storage/postgres/sqlc/test_runs.sql | 11 +- internal/testrunner/seeder.go | 7 + internal/testrunner/tests.go | 417 ++++++------------ internal/worker/k8s.go | 49 +- internal/worker/k8s_test.go | 17 +- internal/worker/manifests.go | 86 ++-- internal/worker/manifests_test.go | 8 +- internal/worker/runner.go | 35 +- 26 files changed, 1237 insertions(+), 368 deletions(-) create mode 100644 deploy/minikube/minio.yaml create mode 100644 deploy/minikube/namespace.yaml create mode 100644 deploy/minikube/postgres.yaml create mode 100644 deploy/minikube/rbac.yaml create mode 100644 deploy/minikube/redis.yaml create mode 100644 deploy/minikube/server.yaml create mode 100644 deploy/minikube/worker.yaml create mode 100644 internal/api/artifacts.go create mode 100644 internal/api/results.go create mode 100644 internal/api/templates/detail.html create mode 100644 internal/api/templates/layout.html create mode 100644 internal/api/templates/list.html diff --git a/cmd/server/main.go b/cmd/server/main.go index 3e5bd0b..a409a1d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -48,7 +48,7 @@ func main() { producer := queue.NewProducer(client) defer producer.Close() - server := api.NewServer(cfg.Server.Host, cfg.Server.Port, db, producer, logger) + server := api.NewServer(cfg, db, producer, logger) err = server.Start(ctx) if err != nil { diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go index 97106a6..0ace64e 100644 --- a/cmd/testrunner/main.go +++ b/cmd/testrunner/main.go @@ -88,16 +88,22 @@ func runTest() { logger.WithError(err).Fatal("failed to generate EVM fixture") } + pluginURL := requireEnv("PLUGIN_ENDPOINT") + client := testrunner.NewTestClient(verifierURL) + pluginCli := testrunner.NewTestClient(pluginURL) - logger.WithField("verifier_url", verifierURL).Info("waiting for verifier health") + logger.WithFields(logrus.Fields{ + "verifier_url": verifierURL, + "plugin_url": pluginURL, + }).Info("waiting for verifier health") err = client.WaitForHealth(60 * time.Second) if err != nil { logger.WithError(err).Fatal("verifier not healthy") } logger.Info("verifier is healthy") - suite := testrunner.NewTestSuite(client, fixture, plugins, jwtToken, evmFixture, logger) + suite := testrunner.NewTestSuite(client, pluginCli, fixture, plugins, jwtToken, evmFixture, logger) suite.RunAll() if suite.Failed > 0 { diff --git a/config/config.go b/config/config.go index 20078ee..dff49d4 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ type APIConfig struct { Server ServerConfig `envconfig:"SERVER"` Database DatabaseConfig `envconfig:"DATABASE"` QueueRedis RedisConfig `envconfig:"QUEUE_REDIS"` + ArtifactS3 S3Config `envconfig:"ARTIFACT_S3"` } type WorkerConfig struct { @@ -67,6 +68,7 @@ type K8sJobConfig struct { EncryptionSecret string `envconfig:"ENCRYPTION_SECRET"` JWTSecret string `envconfig:"JWT_SECRET"` PluginEndpoint string `envconfig:"PLUGIN_ENDPOINT"` + HostAliases string `envconfig:"HOST_ALIASES"` } type JanitorConfig struct { diff --git a/deploy/minikube/minio.yaml b/deploy/minikube/minio.yaml new file mode 100644 index 0000000..9170d16 --- /dev/null +++ b/deploy/minikube/minio.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + namespace: plugin-tests-system +spec: + replicas: 1 + selector: + matchLabels: + app: minio + template: + metadata: + labels: + app: minio + spec: + containers: + - name: minio + image: minio/minio:latest + imagePullPolicy: IfNotPresent + command: ["minio", "server", "/data"] + env: + - name: MINIO_ROOT_USER + value: minioadmin + - name: MINIO_ROOT_PASSWORD + value: minioadmin + ports: + - containerPort: 9000 + readinessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + namespace: plugin-tests-system +spec: + selector: + app: minio + ports: + - port: 9000 + targetPort: 9000 diff --git a/deploy/minikube/namespace.yaml b/deploy/minikube/namespace.yaml new file mode 100644 index 0000000..b85cf0b --- /dev/null +++ b/deploy/minikube/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: plugin-tests-system + labels: + app.kubernetes.io/managed-by: plugin-tests diff --git a/deploy/minikube/postgres.yaml b/deploy/minikube/postgres.yaml new file mode 100644 index 0000000..c8cafd1 --- /dev/null +++ b/deploy/minikube/postgres.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: plugin-tests-system +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:15 + imagePullPolicy: IfNotPresent + env: + - name: POSTGRES_USER + value: vultisig + - name: POSTGRES_PASSWORD + value: vultisig + - name: POSTGRES_DB + value: plugin-tests + ports: + - containerPort: 5432 + readinessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 5 + periodSeconds: 3 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: plugin-tests-system +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/deploy/minikube/rbac.yaml b/deploy/minikube/rbac.yaml new file mode 100644 index 0000000..35c0ec4 --- /dev/null +++ b/deploy/minikube/rbac.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: plugin-tests-worker + namespace: plugin-tests-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: plugin-tests-worker +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["create", "delete", "get", "list"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + - apiGroups: [""] + resources: ["services", "configmaps"] + verbs: ["create"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["create", "get"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "get"] + - apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: plugin-tests-worker +subjects: + - kind: ServiceAccount + name: plugin-tests-worker + namespace: plugin-tests-system +roleRef: + kind: ClusterRole + name: plugin-tests-worker + apiGroup: rbac.authorization.k8s.io diff --git a/deploy/minikube/redis.yaml b/deploy/minikube/redis.yaml new file mode 100644 index 0000000..958cc01 --- /dev/null +++ b/deploy/minikube/redis.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: plugin-tests-system +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: plugin-tests-system +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 diff --git a/deploy/minikube/server.yaml b/deploy/minikube/server.yaml new file mode 100644 index 0000000..8c8d1c4 --- /dev/null +++ b/deploy/minikube/server.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server + namespace: plugin-tests-system +spec: + replicas: 1 + selector: + matchLabels: + app: server + template: + metadata: + labels: + app: server + spec: + containers: + - name: server + image: plugin-tests-server:latest + imagePullPolicy: IfNotPresent + command: ["/app/main"] + env: + - name: PLUGIN_TESTS_API_DATABASE_DSN + value: "postgres://vultisig:vultisig@postgres:5432/plugin-tests?sslmode=disable" + - name: PLUGIN_TESTS_API_QUEUE_REDIS_HOST + value: redis + - name: PLUGIN_TESTS_API_QUEUE_REDIS_PORT + value: "6379" + - name: PLUGIN_TESTS_API_SERVER_HOST + value: "0.0.0.0" + - name: PLUGIN_TESTS_API_SERVER_PORT + value: "8080" + - name: PLUGIN_TESTS_API_ARTIFACT_S3_ENDPOINT + value: "http://minio:9000" + - name: PLUGIN_TESTS_API_ARTIFACT_S3_REGION + value: "us-east-1" + - name: PLUGIN_TESTS_API_ARTIFACT_S3_ACCESS_KEY + value: "minioadmin" + - name: PLUGIN_TESTS_API_ARTIFACT_S3_SECRET_KEY + value: "minioadmin" + - name: PLUGIN_TESTS_API_ARTIFACT_S3_BUCKET + value: "plugin-tests-artifacts" + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: server + namespace: plugin-tests-system +spec: + type: NodePort + selector: + app: server + ports: + - port: 8080 + targetPort: 8080 + nodePort: 30090 diff --git a/deploy/minikube/worker.yaml b/deploy/minikube/worker.yaml new file mode 100644 index 0000000..1f7bbe3 --- /dev/null +++ b/deploy/minikube/worker.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker + namespace: plugin-tests-system +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + serviceAccountName: plugin-tests-worker + containers: + - name: worker + image: plugin-tests-worker:latest + imagePullPolicy: IfNotPresent + command: ["/app/main"] + env: + - name: PLUGIN_TESTS_WORKER_DATABASE_DSN + value: "postgres://vultisig:vultisig@postgres:5432/plugin-tests?sslmode=disable" + - name: PLUGIN_TESTS_WORKER_QUEUE_REDIS_HOST + value: redis + - name: PLUGIN_TESTS_WORKER_QUEUE_REDIS_PORT + value: "6379" + - name: PLUGIN_TESTS_WORKER_CONCURRENCY + value: "1" + - name: PLUGIN_TESTS_WORKER_HEALTH_PORT + value: "8081" + - name: PLUGIN_TESTS_WORKER_K8S_VERIFIER_IMAGE + value: verifier-verifier:latest + - name: PLUGIN_TESTS_WORKER_K8S_VERIFIER_WORKER_IMAGE + value: verifier-worker:latest + - name: PLUGIN_TESTS_WORKER_K8S_TEST_IMAGE + value: plugin-tests-testrunner:latest + - name: PLUGIN_TESTS_WORKER_K8S_ENCRYPTION_SECRET + value: test123 + - name: PLUGIN_TESTS_WORKER_K8S_JWT_SECRET + value: mysecret + - name: PLUGIN_TESTS_WORKER_K8S_PLUGIN_ENDPOINT + value: "http://host.minikube.internal:8082" + - name: PLUGIN_TESTS_WORKER_K8S_HOST_ALIASES + value: "host.minikube.internal=192.168.65.254" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_ENDPOINT + value: "http://minio:9000" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_REGION + value: "us-east-1" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_ACCESS_KEY + value: "minioadmin" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_SECRET_KEY + value: "minioadmin" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_BUCKET + value: "plugin-tests-artifacts" + ports: + - containerPort: 8081 + readinessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi diff --git a/internal/api/artifacts.go b/internal/api/artifacts.go new file mode 100644 index 0000000..9b2bb3c --- /dev/null +++ b/internal/api/artifacts.go @@ -0,0 +1,89 @@ +package api + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" + + "github.com/vultisig/plugin-tests/config" +) + +var allowedArtifacts = map[string]bool{ + "seeder.txt": true, + "test.txt": true, +} + +func (s *Server) handleGetArtifact(c echo.Context) error { + idStr := c.Param("id") + parsed, err := uuid.Parse(idStr) + if err != nil { + return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "invalid run id"}) + } + + name := c.Param("name") + if !allowedArtifacts[name] { + return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "invalid artifact name"}) + } + + pgID := pgtype.UUID{Bytes: parsed, Valid: true} + run, err := s.db.Queries().GetTestRun(c.Request().Context(), pgID) + if err != nil { + if err == pgx.ErrNoRows { + return c.JSON(http.StatusNotFound, ErrorResponse{Error: "test run not found"}) + } + s.logger.WithError(err).Error("failed to get test run") + return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"}) + } + + if !run.ArtifactPrefix.Valid || run.ArtifactPrefix.String == "" { + return c.JSON(http.StatusNotFound, ErrorResponse{Error: "no artifacts for this run"}) + } + + key := run.ArtifactPrefix.String + "/" + name + content, err := readArtifact(c.Request().Context(), s.artifactS3, key) + if err != nil { + s.logger.WithError(err).WithField("key", key).Error("failed to read artifact from S3") + return c.JSON(http.StatusNotFound, ErrorResponse{Error: "artifact not found"}) + } + + return c.String(http.StatusOK, content) +} + +func readArtifact(ctx context.Context, cfg config.S3Config, key string) (string, error) { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + Endpoint: aws.String(cfg.Endpoint), + Credentials: credentials.NewStaticCredentials(cfg.AccessKey, cfg.SecretKey, ""), + S3ForcePathStyle: aws.Bool(true), + }) + if err != nil { + return "", fmt.Errorf("failed to create S3 session: %w", err) + } + + client := s3.New(sess) + out, err := client.GetObjectWithContext(ctx, &s3.GetObjectInput{ + Bucket: aws.String(cfg.Bucket), + Key: aws.String(key), + }) + if err != nil { + return "", fmt.Errorf("failed to get S3 object: %w", err) + } + defer out.Body.Close() + + data, err := io.ReadAll(out.Body) + if err != nil { + return "", fmt.Errorf("failed to read S3 object body: %w", err) + } + + return string(data), nil +} diff --git a/internal/api/handler.go b/internal/api/handler.go index 30d2930..5b0a18a 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -135,18 +135,25 @@ func (s *Server) handleListTestRuns(c echo.Context) error { offset = parsed } + filterParams := buildFilterParams(c) + ctx := c.Request().Context() runs, err := s.db.Queries().ListTestRuns(ctx, &queries.ListTestRunsParams{ - Limit: int32(limit), - Offset: int32(offset), + PluginID: filterParams.PluginID, + Status: filterParams.Status, + QueryLimit: int32(limit), + QueryOffset: int32(offset), }) if err != nil { s.logger.WithError(err).Error("failed to list test runs") return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "failed to list test runs"}) } - total, err := s.db.Queries().CountTestRuns(ctx) + total, err := s.db.Queries().CountTestRuns(ctx, &queries.CountTestRunsParams{ + PluginID: filterParams.PluginID, + Status: filterParams.Status, + }) if err != nil { s.logger.WithError(err).Error("failed to count test runs") return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "failed to count test runs"}) @@ -164,3 +171,22 @@ func (s *Server) handleListTestRuns(c echo.Context) error { Offset: offset, }) } + +type filterParams struct { + PluginID pgtype.Text + Status queries.NullTestRunStatus +} + +func buildFilterParams(c echo.Context) filterParams { + var fp filterParams + if v := strings.TrimSpace(c.QueryParam("plugin_id")); v != "" { + fp.PluginID = pgtype.Text{String: v, Valid: true} + } + if v := strings.TrimSpace(c.QueryParam("status")); v != "" { + fp.Status = queries.NullTestRunStatus{ + TestRunStatus: queries.TestRunStatus(v), + Valid: true, + } + } + return fp +} diff --git a/internal/api/results.go b/internal/api/results.go new file mode 100644 index 0000000..09c5700 --- /dev/null +++ b/internal/api/results.go @@ -0,0 +1,219 @@ +package api + +import ( + "embed" + "fmt" + "html/template" + "math" + "net/http" + "net/url" + "strings" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" + + "github.com/vultisig/plugin-tests/internal/storage/postgres/queries" + "github.com/vultisig/plugin-tests/internal/types" +) + +//go:embed templates/*.html +var templateFS embed.FS + +const perPage = 20 + +var allStatuses = []string{"QUEUED", "RUNNING", "PASSED", "FAILED", "ERROR"} + +var tmplFuncs = template.FuncMap{ + "lower": func(s any) string { return strings.ToLower(fmt.Sprintf("%v", s)) }, + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + "deref": func(s *string) string { + if s == nil { + return "" + } + return *s + }, + "formatTime": func(t time.Time) string { + return t.UTC().Format("2006-01-02 15:04:05 UTC") + }, + "formatTimePtr": func(t *time.Time) string { + if t == nil { + return "-" + } + return t.UTC().Format("2006-01-02 15:04:05 UTC") + }, + "duration": func(start, end *time.Time) string { + if start == nil || end == nil { + return "-" + } + d := end.Sub(*start) + if d < time.Second { + return fmt.Sprintf("%dms", d.Milliseconds()) + } + return fmt.Sprintf("%.1fs", d.Seconds()) + }, + "paginationURL": func(pluginID, status string, page int) string { + params := url.Values{} + if pluginID != "" { + params.Set("plugin_id", pluginID) + } + if status != "" { + params.Set("status", status) + } + if page > 1 { + params.Set("page", fmt.Sprintf("%d", page)) + } + q := params.Encode() + if q != "" { + return "/results?" + q + } + return "/results" + }, +} + +var ( + listTmpl = template.Must( + template.New("").Funcs(tmplFuncs).ParseFS(templateFS, "templates/layout.html", "templates/list.html"), + ) + detailTmpl = template.Must( + template.New("").Funcs(tmplFuncs).ParseFS(templateFS, "templates/layout.html", "templates/detail.html"), + ) +) + +type listPageData struct { + Runs []types.TestRun + PluginIDs []string + Statuses []string + SelectedPluginID string + SelectedStatus string + Page int + TotalPages int +} + +type detailPageData struct { + Run types.TestRun + SeederLogs string + TestLogs string +} + +func (s *Server) handleResultsList(c echo.Context) error { + page := 1 + if v := c.QueryParam("page"); v != "" { + fmt.Sscanf(v, "%d", &page) + if page < 1 { + page = 1 + } + } + offset := (page - 1) * perPage + + pluginID := strings.TrimSpace(c.QueryParam("plugin_id")) + status := strings.TrimSpace(c.QueryParam("status")) + + var filterPluginID pgtype.Text + if pluginID != "" { + filterPluginID = pgtype.Text{String: pluginID, Valid: true} + } + var filterStatus queries.NullTestRunStatus + if status != "" { + filterStatus = queries.NullTestRunStatus{ + TestRunStatus: queries.TestRunStatus(status), + Valid: true, + } + } + + ctx := c.Request().Context() + + runs, err := s.db.Queries().ListTestRuns(ctx, &queries.ListTestRunsParams{ + PluginID: filterPluginID, + Status: filterStatus, + QueryLimit: int32(perPage), + QueryOffset: int32(offset), + }) + if err != nil { + s.logger.WithError(err).Error("failed to list test runs") + return c.String(http.StatusInternalServerError, "failed to load runs") + } + + total, err := s.db.Queries().CountTestRuns(ctx, &queries.CountTestRunsParams{ + PluginID: filterPluginID, + Status: filterStatus, + }) + if err != nil { + s.logger.WithError(err).Error("failed to count test runs") + return c.String(http.StatusInternalServerError, "failed to count runs") + } + + pluginIDs, err := s.db.Queries().GetDistinctPluginIDs(ctx) + if err != nil { + s.logger.WithError(err).Error("failed to get plugin IDs") + pluginIDs = []string{} + } + + items := make([]types.TestRun, 0, len(runs)) + for _, r := range runs { + items = append(items, types.TestRunFromQuery(r)) + } + + totalPages := int(math.Ceil(float64(total) / float64(perPage))) + if totalPages < 1 { + totalPages = 1 + } + + data := listPageData{ + Runs: items, + PluginIDs: pluginIDs, + Statuses: allStatuses, + SelectedPluginID: pluginID, + SelectedStatus: status, + Page: page, + TotalPages: totalPages, + } + + return renderHTML(c, listTmpl, data) +} + +func (s *Server) handleResultsDetail(c echo.Context) error { + idStr := c.Param("id") + parsed, err := uuid.Parse(idStr) + if err != nil { + return c.String(http.StatusBadRequest, "invalid run id") + } + + pgID := pgtype.UUID{Bytes: parsed, Valid: true} + ctx := c.Request().Context() + + run, err := s.db.Queries().GetTestRun(ctx, pgID) + if err != nil { + if err == pgx.ErrNoRows { + return c.String(http.StatusNotFound, "test run not found") + } + s.logger.WithError(err).Error("failed to get test run") + return c.String(http.StatusInternalServerError, "failed to load run") + } + + result := types.TestRunFromQuery(run) + + var seederLogs, testLogs string + if run.ArtifactPrefix.Valid && run.ArtifactPrefix.String != "" && s.artifactS3.Bucket != "" { + prefix := run.ArtifactPrefix.String + seederLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/seeder.txt") + testLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/test.txt") + } + + data := detailPageData{ + Run: result, + SeederLogs: seederLogs, + TestLogs: testLogs, + } + + return renderHTML(c, detailTmpl, data) +} + +func renderHTML(c echo.Context, t *template.Template, data any) error { + c.Response().Header().Set("Content-Type", "text/html; charset=utf-8") + c.Response().WriteHeader(http.StatusOK) + return t.ExecuteTemplate(c.Response().Writer, "layout", data) +} diff --git a/internal/api/server.go b/internal/api/server.go index 1abc6e0..e4eba62 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -11,26 +11,29 @@ import ( "github.com/labstack/echo/v4/middleware" "github.com/sirupsen/logrus" + "github.com/vultisig/plugin-tests/config" "github.com/vultisig/plugin-tests/internal/queue" "github.com/vultisig/plugin-tests/internal/storage" ) type Server struct { - host string - port int - db storage.DatabaseStorage - producer *queue.Producer - logger *logrus.Logger - echo *echo.Echo + host string + port int + db storage.DatabaseStorage + producer *queue.Producer + logger *logrus.Logger + echo *echo.Echo + artifactS3 config.S3Config } -func NewServer(host string, port int, db storage.DatabaseStorage, producer *queue.Producer, logger *logrus.Logger) *Server { +func NewServer(cfg *config.APIConfig, db storage.DatabaseStorage, producer *queue.Producer, logger *logrus.Logger) *Server { return &Server{ - host: host, - port: port, - db: db, - producer: producer, - logger: logger, + host: cfg.Server.Host, + port: cfg.Server.Port, + db: db, + producer: producer, + logger: logger, + artifactS3: cfg.ArtifactS3, } } @@ -62,8 +65,12 @@ func (s *Server) Start(ctx context.Context) error { api := e.Group("/integration-tests") api.POST("/run", s.handleCreateTestRun) api.GET("/run/:id", s.handleGetTestRun) + api.GET("/run/:id/artifacts/:name", s.handleGetArtifact) api.GET("/runs", s.handleListTestRuns) + e.GET("/results", s.handleResultsList) + e.GET("/results/:id", s.handleResultsDetail) + addr := fmt.Sprintf("%s:%d", s.host, s.port) s.logger.Infof("API server listening on %s", addr) diff --git a/internal/api/templates/detail.html b/internal/api/templates/detail.html new file mode 100644 index 0000000..faded5d --- /dev/null +++ b/internal/api/templates/detail.html @@ -0,0 +1,71 @@ +{{define "title"}}Run {{.Run.ID}}{{end}} + +{{define "content"}} +

← Back to results

+ +
+

+ {{.Run.Status}} + {{.Run.PluginID}} +

+
+
+
Run ID
+
{{.Run.ID}}
+
+
+
Requested By
+
{{.Run.RequestedBy}}
+
+
+
Created
+
{{formatTime .Run.CreatedAt}}
+
+
+
Started
+
{{if .Run.StartedAt}}{{formatTimePtr .Run.StartedAt}}{{else}}-{{end}}
+
+
+
Finished
+
{{if .Run.FinishedAt}}{{formatTimePtr .Run.FinishedAt}}{{else}}-{{end}}
+
+
+
Duration
+
{{duration .Run.StartedAt .Run.FinishedAt}}
+
+ {{if .Run.Version}} +
+
Version
+
{{deref .Run.Version}}
+
+ {{end}} + {{if .Run.ProposalID}} +
+
Proposal ID
+
{{deref .Run.ProposalID}}
+
+ {{end}} +
+ {{if .Run.ErrorMessage}} +
{{deref .Run.ErrorMessage}}
+ {{end}} +
+ +{{if .SeederLogs}} +
+

Seeder Logs

+
{{.SeederLogs}}
+
+{{end}} + +{{if .TestLogs}} +
+

Test Logs

+
{{.TestLogs}}
+
+{{end}} + +{{if and (not .SeederLogs) (not .TestLogs)}} +
No artifacts available for this run.
+{{end}} +{{end}} diff --git a/internal/api/templates/layout.html b/internal/api/templates/layout.html new file mode 100644 index 0000000..e5f1ad4 --- /dev/null +++ b/internal/api/templates/layout.html @@ -0,0 +1,60 @@ +{{define "layout"}} + + + + + +{{template "title" .}} - Plugin Tests + + + + +
+ {{template "content" .}} +
+ + +{{end}} diff --git a/internal/api/templates/list.html b/internal/api/templates/list.html new file mode 100644 index 0000000..b7f540d --- /dev/null +++ b/internal/api/templates/list.html @@ -0,0 +1,66 @@ +{{define "title"}}Results{{end}} + +{{define "content"}} +

Test Results

+ +
+ + + + {{if or .SelectedPluginID .SelectedStatus}} + Clear + {{end}} +
+ +{{if .Runs}} + + + + + + + + + + + + + {{range .Runs}} + + + + + + + + + {{end}} + +
StatusPluginRequested ByCreatedDuration
{{.Status}}{{.PluginID}}{{.RequestedBy}}{{formatTime .CreatedAt}}{{duration .StartedAt .FinishedAt}}View
+ +{{if gt .TotalPages 1}} + +{{end}} + +{{else}} +
No test runs found.
+{{end}} +{{end}} diff --git a/internal/storage/postgres/queries/test_runs.sql.go b/internal/storage/postgres/queries/test_runs.sql.go index 811dbbc..40355e3 100644 --- a/internal/storage/postgres/queries/test_runs.sql.go +++ b/internal/storage/postgres/queries/test_runs.sql.go @@ -13,10 +13,17 @@ import ( const countTestRuns = `-- name: CountTestRuns :one SELECT COUNT(*)::bigint FROM test_runs +WHERE ($1::text IS NULL OR plugin_id = $1) + AND ($2::test_run_status IS NULL OR status = $2) ` -func (q *Queries) CountTestRuns(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countTestRuns) +type CountTestRunsParams struct { + PluginID pgtype.Text `json:"plugin_id"` + Status NullTestRunStatus `json:"status"` +} + +func (q *Queries) CountTestRuns(ctx context.Context, arg *CountTestRunsParams) (int64, error) { + row := q.db.QueryRow(ctx, countTestRuns, arg.PluginID, arg.Status) var column_1 int64 err := row.Scan(&column_1) return column_1, err @@ -62,6 +69,30 @@ func (q *Queries) CreateTestRun(ctx context.Context, arg *CreateTestRunParams) ( return &i, err } +const getDistinctPluginIDs = `-- name: GetDistinctPluginIDs :many +SELECT DISTINCT plugin_id FROM test_runs ORDER BY plugin_id +` + +func (q *Queries) GetDistinctPluginIDs(ctx context.Context) ([]string, error) { + rows, err := q.db.Query(ctx, getDistinctPluginIDs) + if err != nil { + return nil, err + } + defer rows.Close() + items := []string{} + for rows.Next() { + var plugin_id string + if err := rows.Scan(&plugin_id); err != nil { + return nil, err + } + items = append(items, plugin_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getStaleRunningRuns = `-- name: GetStaleRunningRuns :many SELECT id, plugin_id, proposal_id, version, status, requested_by, artifact_prefix, error_message, started_at, finished_at, created_at, updated_at FROM test_runs WHERE status = 'RUNNING' AND started_at < NOW() - $1::interval @@ -127,17 +158,26 @@ func (q *Queries) GetTestRun(ctx context.Context, id pgtype.UUID) (*TestRun, err const listTestRuns = `-- name: ListTestRuns :many SELECT id, plugin_id, proposal_id, version, status, requested_by, artifact_prefix, error_message, started_at, finished_at, created_at, updated_at FROM test_runs +WHERE ($1::text IS NULL OR plugin_id = $1) + AND ($2::test_run_status IS NULL OR status = $2) ORDER BY created_at DESC -LIMIT $1 OFFSET $2 +LIMIT $4 OFFSET $3 ` type ListTestRunsParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + PluginID pgtype.Text `json:"plugin_id"` + Status NullTestRunStatus `json:"status"` + QueryOffset int32 `json:"query_offset"` + QueryLimit int32 `json:"query_limit"` } func (q *Queries) ListTestRuns(ctx context.Context, arg *ListTestRunsParams) ([]*TestRun, error) { - rows, err := q.db.Query(ctx, listTestRuns, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, listTestRuns, + arg.PluginID, + arg.Status, + arg.QueryOffset, + arg.QueryLimit, + ) if err != nil { return nil, err } diff --git a/internal/storage/postgres/sqlc/test_runs.sql b/internal/storage/postgres/sqlc/test_runs.sql index a5f6053..c6c0fad 100644 --- a/internal/storage/postgres/sqlc/test_runs.sql +++ b/internal/storage/postgres/sqlc/test_runs.sql @@ -9,11 +9,18 @@ WHERE id = $1; -- name: ListTestRuns :many SELECT * FROM test_runs +WHERE (sqlc.narg('plugin_id')::text IS NULL OR plugin_id = sqlc.narg('plugin_id')) + AND (sqlc.narg('status')::test_run_status IS NULL OR status = sqlc.narg('status')) ORDER BY created_at DESC -LIMIT $1 OFFSET $2; +LIMIT sqlc.arg('query_limit') OFFSET sqlc.arg('query_offset'); -- name: CountTestRuns :one -SELECT COUNT(*)::bigint FROM test_runs; +SELECT COUNT(*)::bigint FROM test_runs +WHERE (sqlc.narg('plugin_id')::text IS NULL OR plugin_id = sqlc.narg('plugin_id')) + AND (sqlc.narg('status')::test_run_status IS NULL OR status = sqlc.narg('status')); + +-- name: GetDistinctPluginIDs :many +SELECT DISTINCT plugin_id FROM test_runs ORDER BY plugin_id; -- name: UpdateTestRunStarted :exec UPDATE test_runs diff --git a/internal/testrunner/seeder.go b/internal/testrunner/seeder.go index f561189..9b55814 100644 --- a/internal/testrunner/seeder.go +++ b/internal/testrunner/seeder.go @@ -204,6 +204,13 @@ func (s *Seeder) SeedVaults(ctx context.Context) error { s3Client := s3.New(sess) + _, err = s3Client.CreateBucket(&s3.CreateBucketInput{ + Bucket: aws.String(s.config.S3.Bucket), + }) + if err != nil { + s.logger.WithError(err).Debug("bucket creation (may already exist)") + } + s.logger.Info("seeding vault fixtures to MinIO") for _, plugin := range s.config.Plugins { diff --git a/internal/testrunner/tests.go b/internal/testrunner/tests.go index 4d18902..f98c80f 100644 --- a/internal/testrunner/tests.go +++ b/internal/testrunner/tests.go @@ -3,14 +3,13 @@ package testrunner import ( "fmt" "net/http" - "strings" - "time" "github.com/sirupsen/logrus" ) type TestSuite struct { client *TestClient + pluginCli *TestClient fixture *FixtureData plugins []PluginConfig jwtToken string @@ -22,9 +21,10 @@ type TestSuite struct { Errors []string } -func NewTestSuite(client *TestClient, fixture *FixtureData, plugins []PluginConfig, jwtToken string, evmFixture *EVMFixture, logger *logrus.Logger) *TestSuite { +func NewTestSuite(client *TestClient, pluginCli *TestClient, fixture *FixtureData, plugins []PluginConfig, jwtToken string, evmFixture *EVMFixture, logger *logrus.Logger) *TestSuite { return &TestSuite{ client: client, + pluginCli: pluginCli, fixture: fixture, plugins: plugins, jwtToken: jwtToken, @@ -54,58 +54,82 @@ func (s *TestSuite) run(name string, fn func() error) { } func (s *TestSuite) RunAll() { - sections := []struct { - name string - fn func() + phases := []struct { + name string + fn func() bool + required bool }{ - {"plugin endpoints", s.testPluginEndpoints}, - {"vault endpoints", s.testVaultEndpoints}, - {"policy endpoints", s.testPolicyEndpoints}, - {"signer endpoints", s.testSignerEndpoints}, + {"health checks", s.healthChecks, true}, + {"verifier-plugin integration", s.verifierPluginTests, false}, + {"plugin canonical endpoints", s.pluginEndpointTests, false}, } - for _, section := range sections { + for _, phase := range phases { beforePassed := s.Passed beforeFailed := s.Failed - s.logger.WithField("section", section.name).Info("starting section") - section.fn() + s.logger.WithField("phase", phase.name).Info("starting phase") + allPassed := phase.fn() - sectionPassed := s.Passed - beforePassed - sectionFailed := s.Failed - beforeFailed + phasePassed := s.Passed - beforePassed + phaseFailed := s.Failed - beforeFailed s.logger.WithFields(logrus.Fields{ - "section": section.name, - "passed": sectionPassed, - "failed": sectionFailed, - }).Info("section completed") + "phase": phase.name, + "passed": phasePassed, + "failed": phaseFailed, + }).Info("phase completed") + + if phase.required && !allPassed { + s.logger.WithField("phase", phase.name).Error("required phase failed, skipping remaining phases") + break + } } s.logger.WithFields(logrus.Fields{ "total": s.Total, "passed": s.Passed, "failed": s.Failed, - }).Info("all sections completed") + }).Info("all phases completed") } -func (s *TestSuite) testPluginEndpoints() { +func (s *TestSuite) healthChecks() bool { + beforeFailed := s.Failed + + s.run("VerifierHealth", func() error { + resp, err := s.client.GET("/plugins") + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("expected 2xx, got %d", resp.StatusCode) + } + return nil + }) + + s.run("PluginHealth", func() error { + resp, err := s.pluginCli.GET("/healthz") + if err != nil { + return fmt.Errorf("plugin unreachable: %w", err) + } + defer resp.Body.Close() + return nil + }) + for _, plugin := range s.plugins { pluginID := plugin.ID - - s.run(pluginID+"/GetPluginDetails", func() error { + s.run("PluginSeeded/"+pluginID, func() error { resp, err := s.client.GET("/plugins/" + pluginID) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + return fmt.Errorf("expected 200, got %d", resp.StatusCode) } - var apiResp struct { Data struct { - ID string `json:"id"` - Title string `json:"title"` + ID string `json:"id"` } `json:"data"` } err = ReadJSONResponse(resp, &apiResp) @@ -115,11 +139,20 @@ func (s *TestSuite) testPluginEndpoints() { if apiResp.Data.ID != pluginID { return fmt.Errorf("expected plugin ID %s, got %s", pluginID, apiResp.Data.ID) } - if apiResp.Data.Title == "" { - return fmt.Errorf("expected non-empty title") - } return nil }) + } + + return s.Failed == beforeFailed +} + +func (s *TestSuite) verifierPluginTests() bool { + beforeFailed := s.Failed + + for i, plugin := range s.plugins { + pluginID := plugin.ID + apiKey := fmt.Sprintf("integration-test-apikey-%s", pluginID) + policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) s.run(pluginID+"/GetRecipeSpecification", func() error { resp, err := s.client.GET("/plugins/" + pluginID + "/recipe-specification") @@ -127,11 +160,9 @@ func (s *TestSuite) testPluginEndpoints() { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + return fmt.Errorf("expected 200, got %d", resp.StatusCode) } - var apiResp struct { Data struct { PluginID string `json:"plugin_id"` @@ -142,331 +173,143 @@ func (s *TestSuite) testPluginEndpoints() { if err != nil { return err } - if apiResp.Data.PluginID != pluginID { - return fmt.Errorf("expected plugin_id %s, got %s", pluginID, apiResp.Data.PluginID) + if apiResp.Data.PluginID == "" { + return fmt.Errorf("expected non-empty plugin_id") } if apiResp.Data.PluginName == "" { return fmt.Errorf("expected non-empty plugin_name") } return nil }) - } -} - -func (s *TestSuite) testVaultEndpoints() { - time.Sleep(2 * time.Second) - - for _, plugin := range s.plugins { - pluginID := plugin.ID - pubkey := s.fixture.Vault.PublicKey - - time.Sleep(500 * time.Millisecond) - s.run(pluginID+"/VaultExists", func() error { - resp, err := s.client.WithJWT(s.jwtToken).GET("/vault/exist/" + pluginID + "/" + pubkey) + s.run(pluginID+"/GetRecipeFunctions", func() error { + resp, err := s.client.GET("/plugins/" + pluginID + "/recipe-functions") if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + return fmt.Errorf("expected 200, got %d", resp.StatusCode) } - var apiResp struct { - Data string `json:"data"` + Data map[string]interface{} `json:"data"` } err = ReadJSONResponse(resp, &apiResp) if err != nil { return err } - if apiResp.Data != "ok" { - return fmt.Errorf("expected data 'ok', got '%s'", apiResp.Data) + if len(apiResp.Data) == 0 { + return fmt.Errorf("expected non-empty recipe functions") } return nil }) - s.run(pluginID+"/GetVault_HappyPath", func() error { - time.Sleep(500 * time.Millisecond) - resp, err := s.client.WithJWT(s.jwtToken).GET("/vault/get/" + pluginID + "/" + pubkey) + s.run(pluginID+"/Reshare", func() error { + reqBody := map[string]interface{}{ + "session_id": s.fixture.Reshare.SessionID, + "hex_encryption_key": s.fixture.Reshare.HexEncryptionKey, + "hex_chain_code": s.fixture.Reshare.HexChainCode, + "local_party_id": s.fixture.Reshare.LocalPartyID, + "old_parties": s.fixture.Reshare.OldParties, + "old_reshare_prefix": s.fixture.Reshare.OldResharePrefix, + "email": s.fixture.Reshare.Email, + "public_key": s.fixture.Vault.PublicKey, + "plugin_id": pluginID, + } + resp, err := s.client.WithJWT(s.jwtToken).POST("/vault/reshare", reqBody) if err != nil { - return fmt.Errorf("request failed: %w", err) + return fmt.Errorf("request failed (verifier->plugin connectivity): %w", err) } defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) - } return nil }) - } -} -func (s *TestSuite) testPolicyEndpoints() { - for i, plugin := range s.plugins { - pluginID := plugin.ID - policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) + s.run(pluginID+"/Sign", func() error { + reqBody := map[string]interface{}{ + "plugin_id": pluginID, + "public_key": s.fixture.Vault.PublicKey, + "policy_id": policyID, + "transactions": s.evmFixture.TxB64, + "transaction_type": "evm", + "messages": []map[string]interface{}{ + { + "message": s.evmFixture.MsgB64, + "chain": "Ethereum", + "hash": s.evmFixture.MsgSHA256B64, + "hash_function": "SHA256", + }, + }, + } - s.run(pluginID+"/GetPolicy_HappyPath", func() error { - resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policy/" + policyID) + resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) + return fmt.Errorf("expected 200, got %d", resp.StatusCode) } var apiResp struct { Data struct { - ID string `json:"id"` - PluginID string `json:"plugin_id"` - Active bool `json:"active"` + TaskIDs []string `json:"task_ids"` } `json:"data"` } err = ReadJSONResponse(resp, &apiResp) if err != nil { return err } - if apiResp.Data.ID != policyID { - return fmt.Errorf("expected policy ID %s, got %s", policyID, apiResp.Data.ID) - } - if apiResp.Data.PluginID != pluginID { - return fmt.Errorf("expected plugin ID %s, got %s", pluginID, apiResp.Data.PluginID) - } - if !apiResp.Data.Active { - return fmt.Errorf("expected policy to be active") - } - return nil - }) - - s.run(pluginID+"/GetAllPolicies_HappyPath", func() error { - resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policies/" + pluginID) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) - } - return nil - }) - - s.run(pluginID+"/CreatePolicy_InvalidSignature", func() error { - reqBody := map[string]interface{}{ - "id": "00000000-0000-0000-0000-000000000001", - "public_key": s.fixture.Vault.PublicKey, - "plugin_id": pluginID, - "plugin_version": "1.0.0", - "policy_version": 1, - "signature": "0x" + strings.Repeat("0", 130), - "recipe": "CgA=", - "billing": []interface{}{}, - "active": true, - } - - resp, err := s.client.WithJWT(s.jwtToken).POST("/plugin/policy", reqBody) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusBadRequest { - return fmt.Errorf("expected status 400, got %d", resp.StatusCode) - } - return nil - }) - - s.run(pluginID+"/CreatePolicy_NoAuth", func() error { - reqBody := map[string]interface{}{ - "id": "00000000-0000-0000-0000-000000000001", - "public_key": s.fixture.Vault.PublicKey, - "plugin_id": pluginID, - "plugin_version": "1.0.0", - "policy_version": 1, - "signature": "0x" + strings.Repeat("0", 130), - "recipe": "CgA=", - "billing": []interface{}{}, - "active": true, - } - - resp, err := s.client.POST("/plugin/policy", reqBody) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnauthorized { - return fmt.Errorf("expected status 401, got %d", resp.StatusCode) - } - return nil - }) - - s.run(pluginID+"/GetPolicy_NoAuth", func() error { - resp, err := s.client.GET("/plugin/policy/test-id") - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnauthorized { - return fmt.Errorf("expected status 401, got %d", resp.StatusCode) - } - return nil - }) - - s.run(pluginID+"/GetPolicy_InvalidID", func() error { - resp, err := s.client.WithJWT(s.jwtToken).GET("/plugin/policy/test-id") - if err != nil { - return fmt.Errorf("request failed: %w", err) + if len(apiResp.Data.TaskIDs) == 0 { + return fmt.Errorf("expected at least 1 task_id") } - defer resp.Body.Close() - if resp.StatusCode != http.StatusBadRequest { - return fmt.Errorf("expected status 400, got %d", resp.StatusCode) + taskID := apiResp.Data.TaskIDs[0] + pollResp, pollErr := s.client.WithAPIKey(apiKey).GET("/plugin-signer/sign/response/" + taskID) + if pollErr != nil { + return fmt.Errorf("sign response poll failed: %w", pollErr) } + defer pollResp.Body.Close() return nil }) } -} -func (s *TestSuite) testSignerEndpoints() { - for i, plugin := range s.plugins { - pluginID := plugin.ID - apiKey := fmt.Sprintf("integration-test-apikey-%s", pluginID) - policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) + return s.Failed == beforeFailed +} - if i > 0 { - time.Sleep(2 * time.Second) - } +func (s *TestSuite) pluginEndpointTests() bool { + beforeFailed := s.Failed - s.run(pluginID+"/Sign_NoAPIKey", func() error { - reqBody := map[string]interface{}{ - "plugin_id": pluginID, - "public_key": s.fixture.Vault.PublicKey, - "policy_id": policyID, - "messages": []interface{}{}, - } + for _, plugin := range s.plugins { + pluginID := plugin.ID + pubkey := s.fixture.Vault.PublicKey - resp, err := s.client.POST("/plugin-signer/sign", reqBody) + s.run(pluginID+"/PluginVaultExist", func() error { + resp, err := s.pluginCli.GET("/vault/exist/" + pluginID + "/" + pubkey) if err != nil { - return fmt.Errorf("request failed: %w", err) + return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnauthorized { - return fmt.Errorf("expected status 401, got %d", resp.StatusCode) - } return nil }) - s.run(pluginID+"/Sign_InvalidAPIKey", func() error { - reqBody := map[string]interface{}{ - "plugin_id": pluginID, - "public_key": s.fixture.Vault.PublicKey, - "policy_id": policyID, - "messages": []interface{}{}, - } - - resp, err := s.client.WithAPIKey("invalid-api-key").POST("/plugin-signer/sign", reqBody) + s.run(pluginID+"/PluginVaultGet", func() error { + resp, err := s.pluginCli.GET("/vault/get/" + pluginID + "/" + pubkey) if err != nil { - return fmt.Errorf("request failed: %w", err) + return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnauthorized { - return fmt.Errorf("expected status 401, got %d", resp.StatusCode) - } return nil }) - s.run(pluginID+"/Sign_EmptyMessages", func() error { - reqBody := map[string]interface{}{ - "plugin_id": pluginID, - "public_key": s.fixture.Vault.PublicKey, - "policy_id": policyID, - "messages": []interface{}{}, - } - - resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) + s.run(pluginID+"/PluginSignResponse", func() error { + resp, err := s.pluginCli.GET("/vault/sign/response/nonexistent-task") if err != nil { - return fmt.Errorf("request failed: %w", err) + return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() - - if resp.StatusCode != http.StatusBadRequest { - return fmt.Errorf("expected status 400, got %d", resp.StatusCode) - } return nil }) - - s.run(pluginID+"/Sign_ValidRequest", func() error { - reqBody := map[string]interface{}{ - "plugin_id": pluginID, - "public_key": s.fixture.Vault.PublicKey, - "policy_id": policyID, - "transactions": s.evmFixture.TxB64, - "transaction_type": "evm", - "messages": []map[string]interface{}{ - { - "message": s.evmFixture.MsgB64, - "chain": "Ethereum", - "hash": s.evmFixture.MsgSHA256B64, - "hash_function": "SHA256", - }, - }, - } - - resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status 200, got %d", resp.StatusCode) - } - - var apiResp struct { - Data struct { - TaskIDs []string `json:"task_ids"` - } `json:"data"` - } - err = ReadJSONResponse(resp, &apiResp) - if err != nil { - return err - } - - if len(apiResp.Data.TaskIDs) != 1 { - return fmt.Errorf("expected 1 task_id, got %d", len(apiResp.Data.TaskIDs)) - } - - taskID := apiResp.Data.TaskIDs[0] - return s.verifySignResponse(apiKey, taskID) - }) - } -} - -func (s *TestSuite) verifySignResponse(apiKey, taskID string) error { - resp, err := s.client.GET("/plugin-signer/sign/response/" + taskID) - if err != nil { - return fmt.Errorf("GetSignResponse_NoAPIKey request failed: %w", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - return fmt.Errorf("GetSignResponse_NoAPIKey: expected status 401, got %d", resp.StatusCode) - } - - resp, err = s.client.WithAPIKey(apiKey).GET("/plugin-signer/sign/response/" + taskID) - if err != nil { - return fmt.Errorf("GetSignResponse_WithAPIKey request failed: %w", err) - } - resp.Body.Close() - if resp.StatusCode < 200 { - return fmt.Errorf("GetSignResponse_WithAPIKey: expected status >= 200, got %d", resp.StatusCode) } - return nil + return s.Failed == beforeFailed } diff --git a/internal/worker/k8s.go b/internal/worker/k8s.go index c315579..26e1a46 100644 --- a/internal/worker/k8s.go +++ b/internal/worker/k8s.go @@ -190,12 +190,40 @@ func fetchJobLogs(ctx context.Context, clientset kubernetes.Interface, namespace return fetchJobLogsByContainer(ctx, clientset, namespace, name, "dummy", maxRetries, retryDelay) } -func createIntraNamespaceNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string) error { +func createIntraNamespaceNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string, pluginPort int32) error { dnsPort := intstr.FromInt32(53) httpsPort := intstr.FromInt32(443) udp := corev1.ProtocolUDP tcp := corev1.ProtocolTCP + egressRules := []networkingv1.NetworkPolicyEgressRule{ + { + To: []networkingv1.NetworkPolicyPeer{ + {PodSelector: &metav1.LabelSelector{}}, + }, + }, + { + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &udp, Port: &dnsPort}, + {Protocol: &tcp, Port: &dnsPort}, + }, + }, + { + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &tcp, Port: &httpsPort}, + }, + }, + } + + if pluginPort > 0 && pluginPort != 443 { + pp := intstr.FromInt32(pluginPort) + egressRules = append(egressRules, networkingv1.NetworkPolicyEgressRule{ + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &tcp, Port: &pp}, + }, + }) + } + policy := &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "allow-intra-namespace", @@ -214,24 +242,7 @@ func createIntraNamespaceNetworkPolicy(ctx context.Context, clientset kubernetes }, }, }, - Egress: []networkingv1.NetworkPolicyEgressRule{ - { - To: []networkingv1.NetworkPolicyPeer{ - {PodSelector: &metav1.LabelSelector{}}, - }, - }, - { - Ports: []networkingv1.NetworkPolicyPort{ - {Protocol: &udp, Port: &dnsPort}, - {Protocol: &tcp, Port: &dnsPort}, - }, - }, - { - Ports: []networkingv1.NetworkPolicyPort{ - {Protocol: &tcp, Port: &httpsPort}, - }, - }, - }, + Egress: egressRules, }, } _, err := clientset.NetworkingV1().NetworkPolicies(namespace).Create(ctx, policy, metav1.CreateOptions{}) diff --git a/internal/worker/k8s_test.go b/internal/worker/k8s_test.go index e275460..f8abf9c 100644 --- a/internal/worker/k8s_test.go +++ b/internal/worker/k8s_test.go @@ -236,7 +236,7 @@ func TestCreateIntraNamespaceNetworkPolicy(t *testing.T) { t.Run("success", func(t *testing.T) { clientset := fake.NewSimpleClientset() - err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns", 0) require.NoError(t, err) policy, err := clientset.NetworkingV1().NetworkPolicies("test-ns").Get(context.Background(), "allow-intra-namespace", metav1.GetOptions{}) @@ -247,13 +247,24 @@ func TestCreateIntraNamespaceNetworkPolicy(t *testing.T) { require.Len(t, policy.Spec.Egress, 3) }) + t.Run("with plugin port", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns", 8082) + require.NoError(t, err) + + policy, err := clientset.NetworkingV1().NetworkPolicies("test-ns").Get(context.Background(), "allow-intra-namespace", metav1.GetOptions{}) + require.NoError(t, err) + require.Len(t, policy.Spec.Egress, 4) + }) + t.Run("already exists", func(t *testing.T) { clientset := fake.NewSimpleClientset() - err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + err := createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns", 0) require.NoError(t, err) - err = createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns") + err = createIntraNamespaceNetworkPolicy(context.Background(), clientset, "test-ns", 0) assert.NoError(t, err) }) } diff --git a/internal/worker/manifests.go b/internal/worker/manifests.go index da34fd5..701f4e6 100644 --- a/internal/worker/manifests.go +++ b/internal/worker/manifests.go @@ -3,6 +3,7 @@ package worker import ( "encoding/json" "fmt" + "strings" "github.com/vultisig/plugin-tests/config" @@ -72,6 +73,30 @@ type workerJSON struct { } func int32Ptr(i int32) *int32 { return &i } +func boolPtr(b bool) *bool { return &b } + +func parseHostAliases(raw string) []corev1.HostAlias { + if raw == "" { + return nil + } + var aliases []corev1.HostAlias + for _, entry := range strings.Split(raw, ",") { + parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) + if len(parts) != 2 { + continue + } + hostname := strings.TrimSpace(parts[0]) + ip := strings.TrimSpace(parts[1]) + if hostname == "" || ip == "" { + continue + } + aliases = append(aliases, corev1.HostAlias{ + IP: ip, + Hostnames: []string{hostname}, + }) + } + return aliases +} func infraPostgresObjects(ns, image string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { selectorLabels := map[string]string{"app": "postgres"} @@ -86,8 +111,9 @@ func infraPostgresObjects(ns, image string, labels map[string]string) (*appsv1.D ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ - Name: "postgres", - Image: image, + Name: "postgres", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{ {Name: "POSTGRES_USER", Value: "vultisig"}, {Name: "POSTGRES_PASSWORD", Value: "vultisig"}, @@ -131,9 +157,10 @@ func infraRedisObjects(ns, image string, labels map[string]string) (*appsv1.Depl ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ - Name: "redis", - Image: image, - Ports: []corev1.ContainerPort{{ContainerPort: 6379}}, + Name: "redis", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{{ContainerPort: 6379}}, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt32(6379)}, @@ -171,9 +198,10 @@ func infraMinioObjects(ns, image string, labels map[string]string) (*appsv1.Depl ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ - Name: "minio", - Image: image, - Command: []string{"minio", "server", "/data"}, + Name: "minio", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{"minio", "server", "/data"}, Env: []corev1.EnvVar{ {Name: "MINIO_ROOT_USER", Value: "minioadmin"}, {Name: "MINIO_ROOT_PASSWORD", Value: "minioadmin"}, @@ -261,7 +289,7 @@ func workerConfigMap(ns string, cfg config.K8sJobConfig) (*corev1.ConfigMap, err }, nil } -func verifierDeploymentObjects(ns, image, pullSecret string, labels map[string]string) (*appsv1.Deployment, *corev1.Service) { +func verifierDeploymentObjects(ns, image, pullSecret string, labels map[string]string, hostAliases []corev1.HostAlias) (*appsv1.Deployment, *corev1.Service) { selectorLabels := map[string]string{"app": "verifier"} allLabels := mergeLabels(labels, selectorLabels) @@ -273,10 +301,13 @@ func verifierDeploymentObjects(ns, image, pullSecret string, labels map[string]s Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, Spec: corev1.PodSpec{ + EnableServiceLinks: boolPtr(false), + HostAliases: hostAliases, Containers: []corev1.Container{{ - Name: "verifier", - Image: image, - Ports: []corev1.ContainerPort{{ContainerPort: 8080}}, + Name: "verifier", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{{ContainerPort: 8080}}, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -333,10 +364,12 @@ func verifierWorkerDeployment(ns, image, pullSecret string, labels map[string]st Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: allLabels}, Spec: corev1.PodSpec{ + EnableServiceLinks: boolPtr(false), Containers: []corev1.Container{{ - Name: "worker", - Image: image, - Resources: verifierResources(), + Name: "worker", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: verifierResources(), VolumeMounts: []corev1.VolumeMount{{ Name: "config", MountPath: "/app/config.json", @@ -363,15 +396,15 @@ func verifierWorkerDeployment(ns, image, pullSecret string, labels map[string]st return dep } -func seederJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { - return buildTestJob("seeder", ns, image, pullSecret, labels, []string{"seed"}, envVars, ttlSeconds) +func seederJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { + return buildTestJob("seeder", ns, image, pullSecret, labels, []string{"seed"}, envVars, ttlSeconds, hostAliases) } -func testJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { - return buildTestJob("test", ns, image, pullSecret, labels, []string{"test"}, envVars, ttlSeconds) +func testJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { + return buildTestJob("test", ns, image, pullSecret, labels, []string{"test"}, envVars, ttlSeconds, hostAliases) } -func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, args []string, envVars []corev1.EnvVar, ttlSeconds int32) *batchv1.Job { +func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, args []string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { var backoffLimit int32 var ttlPtr *int32 if ttlSeconds > 0 { @@ -391,12 +424,15 @@ func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, ObjectMeta: metav1.ObjectMeta{Labels: labels}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, + HostAliases: hostAliases, Containers: []corev1.Container{{ - Name: "testrunner", - Image: image, - Args: args, - Env: envVars, - Resources: jobResources(), + Name: "testrunner", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{"/app/main"}, + Args: args, + Env: envVars, + Resources: jobResources(), }}, }, }, diff --git a/internal/worker/manifests_test.go b/internal/worker/manifests_test.go index dde58c1..3b9c6ae 100644 --- a/internal/worker/manifests_test.go +++ b/internal/worker/manifests_test.go @@ -105,7 +105,7 @@ func TestVerifierDeploymentObjects(t *testing.T) { labels := map[string]string{"run": "123"} t.Run("with pull secret", func(t *testing.T) { - dep, svc := verifierDeploymentObjects("ns", "verifier:v1", "my-secret", labels) + dep, svc := verifierDeploymentObjects("ns", "verifier:v1", "my-secret", labels, nil) assert.Equal(t, "verifier", dep.Name) require.Len(t, dep.Spec.Template.Spec.Containers, 1) @@ -125,7 +125,7 @@ func TestVerifierDeploymentObjects(t *testing.T) { }) t.Run("without pull secret", func(t *testing.T) { - dep, _ := verifierDeploymentObjects("ns", "verifier:v1", "", labels) + dep, _ := verifierDeploymentObjects("ns", "verifier:v1", "", labels, nil) assert.Empty(t, dep.Spec.Template.Spec.ImagePullSecrets) }) } @@ -143,7 +143,7 @@ func TestVerifierWorkerDeployment(t *testing.T) { func TestSeederJob(t *testing.T) { envVars := testrunnerEnvVars(testK8sCfg) - job := seederJob("ns", "testrunner:v1", "", nil, envVars, 300) + job := seederJob("ns", "testrunner:v1", "", nil, envVars, 300, nil) assert.Equal(t, "seeder", job.Name) assert.Equal(t, "ns", job.Namespace) @@ -160,7 +160,7 @@ func TestSeederJob(t *testing.T) { func TestTestJob(t *testing.T) { envVars := testrunnerEnvVars(testK8sCfg) - job := testJob("ns", "testrunner:v1", "my-secret", nil, envVars, 0) + job := testJob("ns", "testrunner:v1", "my-secret", nil, envVars, 0, nil) assert.Equal(t, "test", job.Name) require.Len(t, job.Spec.Template.Spec.Containers, 1) diff --git a/internal/worker/runner.go b/internal/worker/runner.go index 3735cc0..e8b9b50 100644 --- a/internal/worker/runner.go +++ b/internal/worker/runner.go @@ -3,6 +3,8 @@ package worker import ( "context" "fmt" + "net/url" + "strconv" "time" "github.com/sirupsen/logrus" @@ -96,7 +98,29 @@ func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, lab func (r *Runner) deployNetworkPolicy(ctx context.Context, ns string) error { r.logger.Info("creating network policy") - return createIntraNamespaceNetworkPolicy(ctx, r.k8s, ns) + pluginPort := parsePort(r.cfg.PluginEndpoint) + return createIntraNamespaceNetworkPolicy(ctx, r.k8s, ns, pluginPort) +} + +func parsePort(rawURL string) int32 { + if rawURL == "" { + return 0 + } + u, err := url.Parse(rawURL) + if err != nil { + return 0 + } + if u.Port() != "" { + p, err := strconv.Atoi(u.Port()) + if err != nil { + return 0 + } + return int32(p) + } + if u.Scheme == "https" { + return 443 + } + return 80 } func (r *Runner) deployInfra(ctx context.Context, ns string, labels map[string]string) error { @@ -175,7 +199,8 @@ func (r *Runner) deployConfigMaps(ctx context.Context, ns string) error { } func (r *Runner) deployVerifier(ctx context.Context, ns string, labels map[string]string) error { - vDep, vSvc := verifierDeploymentObjects(ns, r.cfg.VerifierImage, r.cfg.ImagePullSecret, labels) + hostAliases := parseHostAliases(r.cfg.HostAliases) + vDep, vSvc := verifierDeploymentObjects(ns, r.cfg.VerifierImage, r.cfg.ImagePullSecret, labels, hostAliases) err := applyDeployment(ctx, r.k8s, vDep) if err != nil { return fmt.Errorf("deploy verifier: %w", err) @@ -209,7 +234,8 @@ func (r *Runner) waitForVerifier(ctx context.Context, ns string) error { func (r *Runner) runSeederJob(ctx context.Context, ns, runID string, labels map[string]string) (string, error) { envVars := testrunnerEnvVars(r.cfg) name := seederJobName(runID) - job := seederJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished) + hostAliases := parseHostAliases(r.cfg.HostAliases) + job := seederJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) job.Name = name r.logger.WithField("job", name).Info("running seeder") @@ -238,7 +264,8 @@ func (r *Runner) runSeederJob(ctx context.Context, ns, runID string, labels map[ func (r *Runner) runTestJob(ctx context.Context, ns, runID string, labels map[string]string) (string, bool, error) { envVars := testrunnerEnvVars(r.cfg) name := testJobName(runID) - job := testJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished) + hostAliases := parseHostAliases(r.cfg.HostAliases) + job := testJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) job.Name = name r.logger.WithField("job", name).Info("running tests") From f7b8e9db895442dfc4cb9dedbbf4140bade3e41b Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:31:11 +0400 Subject: [PATCH 3/7] create mpc simulator --- Makefile | 2 +- cmd/testrunner/main.go | 44 ++- go.mod | 10 + go.sum | 9 +- internal/api/artifacts.go | 2 +- internal/testrunner/mpc/wrappers.go | 144 ++++++++ internal/testrunner/participant.go | 552 ++++++++++++++++++++++++++++ 7 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 internal/testrunner/mpc/wrappers.go create mode 100644 internal/testrunner/participant.go diff --git a/Makefile b/Makefile index 0cdbb4a..c26da33 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ build-worker: go build -o bin/worker ./cmd/worker build-testrunner: - CGO_ENABLED=0 go build -o bin/testrunner ./cmd/testrunner + CGO_ENABLED=1 go build -o bin/testrunner ./cmd/testrunner sqlc: sqlc generate diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go index 0ace64e..6eea75f 100644 --- a/cmd/testrunner/main.go +++ b/cmd/testrunner/main.go @@ -16,7 +16,7 @@ func main() { logger.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) if len(os.Args) < 2 { - logger.Fatal("usage: testrunner ") + logger.Fatal("usage: testrunner ") } switch os.Args[1] { @@ -24,6 +24,8 @@ func main() { runSeed() case "test": runTest() + case "install": + runInstall() default: logger.Fatalf("unknown command: %s", os.Args[1]) } @@ -123,6 +125,46 @@ func runTest() { }).Info("all tests passed") } +func runInstall() { + fixture, err := testrunner.LoadFixture() + if err != nil { + logger.WithError(err).Fatal("failed to load fixture") + } + + verifierURL := requireEnv("VERIFIER_URL") + relayURL := requireEnv("RELAY_URL") + jwtSecret := requireEnv("JWT_SECRET") + pluginID := requireEnv("PLUGIN_ID") + encryptionSecret := os.Getenv("ENCRYPTION_SECRET") + + jwtToken, err := testrunner.GenerateJWT(jwtSecret, fixture.Vault.PublicKey, "integration-token-1", 24) + if err != nil { + logger.WithError(err).Fatal("failed to generate JWT") + } + + cfg := testrunner.InstallConfig{ + VerifierURL: verifierURL, + RelayURL: relayURL, + JWTToken: jwtToken, + PluginID: pluginID, + Fixture: fixture, + EncryptionSecret: encryptionSecret, + } + + logger.WithFields(logrus.Fields{ + "verifier_url": verifierURL, + "relay_url": relayURL, + "plugin_id": pluginID, + }).Info("starting plugin install (MPC reshare)") + + err = testrunner.RunInstall(cfg, logger) + if err != nil { + logger.WithError(err).Fatal("install failed") + } + + logger.Info("install completed successfully") +} + func requireEnv(key string) string { val := os.Getenv(key) if val == "" { diff --git a/go.mod b/go.mod index 62e38e6..034016d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,9 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778 + github.com/vultisig/go-wrappers v0.0.0-20260116015747-e12e4d06cf57 github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292 + github.com/vultisig/vultiserver v0.0.0-20250825042420-c6e6ac281110 github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85 google.golang.org/protobuf v1.36.10 k8s.io/api v0.35.1 @@ -75,6 +77,7 @@ require ( github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getsentry/sentry-go v0.32.0 // indirect github.com/go-kit/kit v0.13.0 // indirect @@ -84,6 +87,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -127,6 +131,7 @@ require ( github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/otiai10/primes v0.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -138,11 +143,16 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/zerolog v1.34.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/viper v1.20.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect diff --git a/go.sum b/go.sum index 2af1e8d..86fd6b8 100644 --- a/go.sum +++ b/go.sum @@ -27,12 +27,15 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= +github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -661,10 +664,14 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778 h1:XJ1hoo37JKGLmfxD4wYhXO8TJFBdUBnbxxK+zagJ4c4= github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778/go.mod h1:UMc5q0Myab+BvzAe67UQrXTXwKGYNxK7bky7DJM+dl8= +github.com/vultisig/go-wrappers v0.0.0-20260116015747-e12e4d06cf57 h1:ZFlNC4JuaYlURN99W8xizSvOMyMppIkcCf3fEcpTnvo= +github.com/vultisig/go-wrappers v0.0.0-20260116015747-e12e4d06cf57/go.mod h1:vEP0x0RmNlghWxfalt13FvVsBwmobSwimMhJxGfqCD4= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 h1:goqwk4nQ/NEVIb3OPP9SUx7/u9ZfsUIcd5fIN/e4DVU= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74/go.mod h1:nOykk4nOy1L3yXtLSlYvVsgizBnCQ3tR2N5uwGPdvaM= github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292 h1:CVXtJBOjJfzMsv+akQK65Jcup9/TSeMUXD8NMJ9O0eg= github.com/vultisig/recipes v0.0.0-20260129020926-577976dfb292/go.mod h1:PUz2BoPkuCrk9EFeWavynFSIMM2DNULfFeSS0CGVIVc= +github.com/vultisig/vultiserver v0.0.0-20250825042420-c6e6ac281110 h1:7WDQ92FAdu08Byjgm3RNS8Sok49sK521PzPcbRpbzCE= +github.com/vultisig/vultiserver v0.0.0-20250825042420-c6e6ac281110/go.mod h1:HwP2IgW6Mcu/gX8paFuKvfibrGE9UmPgkOFTub6dskM= github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85 h1:NAVXp2Mm791Z/teQHUA8T7mhmx3bouyrXvQkLXvAu3M= github.com/vultisig/vultisig-go v0.0.0-20260114092710-6c38516a0c85/go.mod h1:jOf+2n1Eo/XZjiUbHjTfraPMw4HAZZ0Sw9Z6+vpQrU4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/internal/api/artifacts.go b/internal/api/artifacts.go index 9b2bb3c..1f537f0 100644 --- a/internal/api/artifacts.go +++ b/internal/api/artifacts.go @@ -80,7 +80,7 @@ func readArtifact(ctx context.Context, cfg config.S3Config, key string) (string, } defer out.Body.Close() - data, err := io.ReadAll(out.Body) + data, err := io.ReadAll(io.LimitReader(out.Body, 2<<20)) if err != nil { return "", fmt.Errorf("failed to read S3 object body: %w", err) } diff --git a/internal/testrunner/mpc/wrappers.go b/internal/testrunner/mpc/wrappers.go new file mode 100644 index 0000000..66e4b2f --- /dev/null +++ b/internal/testrunner/mpc/wrappers.go @@ -0,0 +1,144 @@ +package mpc + +import ( + "encoding/base64" + "fmt" + + session "github.com/vultisig/go-wrappers/go-dkls/sessions" + eddsaSession "github.com/vultisig/go-wrappers/go-schnorr/sessions" + vgcommon "github.com/vultisig/vultisig-go/common" +) + +type Handle int32 + +type Wrapper struct { + isEdDSA bool +} + +func NewWrapper(isEdDSA bool) *Wrapper { + return &Wrapper{isEdDSA: isEdDSA} +} + +func (w *Wrapper) QcSetupMsgNew(keyshareHandle Handle, threshold int, ids []string, oldParties []int, newParties []int) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrQcSetupMsgNew(eddsaSession.Handle(keyshareHandle), threshold, ids, oldParties, newParties) + } + return session.DklsQcSetupMsgNew(session.Handle(keyshareHandle), threshold, ids, oldParties, newParties) +} + +func (w *Wrapper) QcSessionFromSetup(setupMsg []byte, id string, keyshareHandle Handle) (Handle, error) { + if w.isEdDSA { + h, err := eddsaSession.SchnorrQcSessionFromSetup(setupMsg, id, eddsaSession.Handle(keyshareHandle)) + return Handle(h), err + } + h, err := session.DklsQcSessionFromSetup(setupMsg, id, session.Handle(keyshareHandle)) + return Handle(h), err +} + +func (w *Wrapper) QcSessionOutputMessage(h Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrQcSessionOutputMessage(eddsaSession.Handle(h)) + } + return session.DklsQcSessionOutputMessage(session.Handle(h)) +} + +func (w *Wrapper) QcSessionMessageReceiver(h Handle, message []byte, index int) (string, error) { + if w.isEdDSA { + return eddsaSession.SchnorrQcSessionMessageReceiver(eddsaSession.Handle(h), message, index) + } + return session.DklsQcSessionMessageReceiver(session.Handle(h), message, index) +} + +func (w *Wrapper) QcSessionInputMessage(h Handle, message []byte) (bool, error) { + if w.isEdDSA { + return eddsaSession.SchnorrQcSessionInputMessage(eddsaSession.Handle(h), message) + } + return session.DklsQcSessionInputMessage(session.Handle(h), message) +} + +// QcSessionFinish completes the QC session and returns a keyshare handle. +// The returned handle must be freed with KeyshareFree when no longer needed. +// There is no separate QcSessionFree in go-wrappers — the QC session is +// consumed by Finish and its resources released through the resulting keyshare. +func (w *Wrapper) QcSessionFinish(h Handle) (Handle, error) { + if w.isEdDSA { + result, err := eddsaSession.SchnorrQcSessionFinish(eddsaSession.Handle(h)) + return Handle(result), err + } + result, err := session.DklsQcSessionFinish(session.Handle(h)) + return Handle(result), err +} + +func (w *Wrapper) KeyshareFromBytes(buf []byte) (Handle, error) { + if w.isEdDSA { + h, err := eddsaSession.SchnorrKeyshareFromBytes(buf) + return Handle(h), err + } + h, err := session.DklsKeyshareFromBytes(buf) + return Handle(h), err +} + +func (w *Wrapper) KeyshareToBytes(share Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrKeyshareToBytes(eddsaSession.Handle(share)) + } + return session.DklsKeyshareToBytes(session.Handle(share)) +} + +func (w *Wrapper) KeysharePublicKey(share Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrKeysharePublicKey(eddsaSession.Handle(share)) + } + return session.DklsKeysharePublicKey(session.Handle(share)) +} + +// KeyshareChainCode returns the chain code for ECDSA keyshares. +// EdDSA (Schnorr) keyshares do not have chain codes — returns empty slice. +func (w *Wrapper) KeyshareChainCode(share Handle) ([]byte, error) { + if w.isEdDSA { + return []byte{}, nil + } + return session.DklsKeyshareChainCode(session.Handle(share)) +} + +// KeyshareFree releases native memory for a keyshare handle. +// For ECDSA: calls DklsKeyshareFree. +// For EdDSA: go-wrappers (go-schnorr) does not expose SchnorrKeyshareFree; +// Schnorr keyshare handles are managed internally by the library. +func (w *Wrapper) KeyshareFree(share Handle) error { + if w.isEdDSA { + return nil + } + return session.DklsKeyshareFree(session.Handle(share)) +} + +// DecodeDecryptMessage processes an inbound relay message. +// Wire format: base64( gcm_encrypt( base64(raw_payload) ) ) +// Steps: base64 decode → AES-GCM decrypt → base64 decode → raw bytes +func DecodeDecryptMessage(body string, hexEncryptionKey string) ([]byte, error) { + decodedBody, err := base64.StdEncoding.DecodeString(body) + if err != nil { + return nil, fmt.Errorf("failed to base64-decode relay message: %w", err) + } + rawBody, err := vgcommon.DecryptGCM(decodedBody, hexEncryptionKey) + if err != nil { + return nil, fmt.Errorf("failed to AES-GCM decrypt relay message: %w", err) + } + inboundBody, err := base64.StdEncoding.DecodeString(string(rawBody)) + if err != nil { + return nil, fmt.Errorf("failed to base64-decode inner payload: %w", err) + } + return inboundBody, nil +} + +// EncryptEncodeSetupMessage prepares a setup message for relay upload. +// Wire format: base64( gcm_encrypt( base64(raw_setup_bytes) ) ) +// This is the inverse of DecodeDecryptMessage. +func EncryptEncodeSetupMessage(setupMsg []byte, hexEncryptionKey string) (string, error) { + innerB64 := base64.StdEncoding.EncodeToString(setupMsg) + encrypted, err := vgcommon.EncryptGCM(innerB64, hexEncryptionKey) + if err != nil { + return "", fmt.Errorf("failed to AES-GCM encrypt setup message: %w", err) + } + return encrypted, nil +} diff --git a/internal/testrunner/participant.go b/internal/testrunner/participant.go new file mode 100644 index 0000000..9f52f7c --- /dev/null +++ b/internal/testrunner/participant.go @@ -0,0 +1,552 @@ +package testrunner + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "slices" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" + "github.com/vultisig/vultiserver/relay" + vgcommon "github.com/vultisig/vultisig-go/common" + vgrelay "github.com/vultisig/vultisig-go/relay" + "google.golang.org/protobuf/proto" + + "github.com/vultisig/plugin-tests/internal/testrunner/mpc" +) + +type InstallConfig struct { + VerifierURL string + RelayURL string + JWTToken string + PluginID string + Fixture *FixtureData + EncryptionSecret string +} + +func RunInstall(cfg InstallConfig, logger *logrus.Logger) error { + vault, err := parseVaultFromFixture(cfg.Fixture, cfg.EncryptionSecret) + if err != nil { + return fmt.Errorf("failed to parse vault from fixture: %w", err) + } + + logger.WithFields(logrus.Fields{ + "local_party_id": vault.LocalPartyId, + "public_key_ecdsa": vault.PublicKeyEcdsa, + "public_key_eddsa": vault.PublicKeyEddsa, + "signers": vault.Signers, + }).Info("parsed vault from fixture") + + sessionID := uuid.New().String() + hexEncKey, err := generateHexEncryptionKey() + if err != nil { + return fmt.Errorf("failed to generate encryption key: %w", err) + } + + relayClient := vgrelay.NewRelayClient(cfg.RelayURL) + + err = relayClient.RegisterSessionWithRetry(sessionID, vault.LocalPartyId) + if err != nil { + return fmt.Errorf("failed to register with relay: %w", err) + } + logger.WithField("session_id", sessionID).Info("registered with relay") + + err = initiateReshare(cfg, vault, sessionID, hexEncKey) + if err != nil { + return fmt.Errorf("failed to initiate reshare: %w", err) + } + logger.Info("reshare initiated on verifier") + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + parties, err := waitForParties(ctx, relayClient, sessionID, 3, logger) + if err != nil { + return fmt.Errorf("failed waiting for parties: %w", err) + } + logger.WithField("parties", parties).Info("all parties registered") + + err = relayClient.StartSession(sessionID, parties) + if err != nil { + return fmt.Errorf("failed to start session: %w", err) + } + logger.Info("session started") + + localPartyID := vault.LocalPartyId + ourIndex := slices.Index(parties, localPartyID) + if ourIndex < 0 { + return fmt.Errorf("our party %s not found in registered parties %v", localPartyID, parties) + } + oldParties := []int{ourIndex} + var newParties []int + for i := range parties { + if i != ourIndex { + newParties = append(newParties, i) + } + } + threshold := int(math.Ceil(float64(len(parties))*2.0/3.0)) - 1 + + logger.WithFields(logrus.Fields{ + "threshold": threshold, + "old_parties": oldParties, + "new_parties": newParties, + "our_index": ourIndex, + }).Info("starting ECDSA reshare") + + ecdsaPubkey, chainCode, err := reshareWithRetry( + vault, sessionID, hexEncKey, parties, vault.PublicKeyEcdsa, + false, localPartyID, threshold, oldParties, newParties, + cfg.RelayURL, logger, + ) + if err != nil { + return fmt.Errorf("ECDSA reshare failed: %w", err) + } + logger.WithField("ecdsa_pubkey", ecdsaPubkey).Info("ECDSA reshare completed") + + logger.Info("starting EdDSA reshare") + eddsaPubkey, _, err := reshareWithRetry( + vault, sessionID, hexEncKey, parties, vault.PublicKeyEddsa, + true, localPartyID, threshold, oldParties, newParties, + cfg.RelayURL, logger, + ) + if err != nil { + return fmt.Errorf("EdDSA reshare failed: %w", err) + } + logger.WithField("eddsa_pubkey", eddsaPubkey).Info("EdDSA reshare completed") + + err = relayClient.CompleteSession(sessionID, localPartyID) + if err != nil { + logger.WithError(err).Warn("failed to complete session (non-fatal)") + } + + _ = chainCode + logger.WithFields(logrus.Fields{ + "ecdsa_pubkey": ecdsaPubkey, + "eddsa_pubkey": eddsaPubkey, + }).Info("install completed successfully") + + return nil +} + +func parseVaultFromFixture(fixture *FixtureData, encryptionSecret string) (*vaultType.Vault, error) { + containerBytes, err := base64.StdEncoding.DecodeString(fixture.Vault.VaultB64) + if err != nil { + return nil, fmt.Errorf("failed to decode vault_b64: %w", err) + } + + var container vaultType.VaultContainer + err = proto.Unmarshal(containerBytes, &container) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal vault container: %w", err) + } + + var vaultBytes []byte + if container.IsEncrypted { + if encryptionSecret == "" { + return nil, fmt.Errorf("vault is encrypted but no encryption secret provided") + } + encBytes, decErr := base64.StdEncoding.DecodeString(container.Vault) + if decErr != nil { + return nil, fmt.Errorf("failed to decode encrypted vault string: %w", decErr) + } + vaultBytes, err = vgcommon.DecryptVault(encryptionSecret, encBytes) + if err != nil { + return nil, fmt.Errorf("failed to decrypt vault: %w", err) + } + } else { + vaultBytes, err = base64.StdEncoding.DecodeString(container.Vault) + if err != nil { + return nil, fmt.Errorf("failed to decode vault string: %w", err) + } + } + + var vault vaultType.Vault + err = proto.Unmarshal(vaultBytes, &vault) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal vault: %w", err) + } + + if vault.LocalPartyId == "" { + return nil, fmt.Errorf("vault has empty local_party_id") + } + if vault.PublicKeyEcdsa == "" { + return nil, fmt.Errorf("vault has empty public_key_ecdsa") + } + + return &vault, nil +} + +func generateHexEncryptionKey() (string, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return "", fmt.Errorf("failed to generate random key: %w", err) + } + return hex.EncodeToString(key), nil +} + +type reshareRequest struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` + SessionID string `json:"session_id"` + HexEncryptionKey string `json:"hex_encryption_key"` + HexChainCode string `json:"hex_chain_code"` + LocalPartyId string `json:"local_party_id"` + OldParties []string `json:"old_parties"` + Email string `json:"email"` + PluginID string `json:"plugin_id"` +} + +func initiateReshare(cfg InstallConfig, vault *vaultType.Vault, sessionID string, hexEncKey string) error { + req := reshareRequest{ + Name: vault.Name, + PublicKey: vault.PublicKeyEcdsa, + SessionID: sessionID, + HexEncryptionKey: hexEncKey, + HexChainCode: vault.HexChainCode, + LocalPartyId: vault.LocalPartyId, + OldParties: vault.Signers, + Email: cfg.Fixture.Reshare.Email, + PluginID: cfg.PluginID, + } + + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal reshare request: %w", err) + } + + url := cfg.VerifierURL + "/vault/reshare" + httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+cfg.JWTToken) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("reshare request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var respBody bytes.Buffer + io.Copy(&respBody, io.LimitReader(resp.Body, 4096)) + return fmt.Errorf("reshare request returned %d: %s", resp.StatusCode, respBody.String()) + } + + return nil +} + +func waitForParties(ctx context.Context, client *vgrelay.Client, sessionID string, expectedCount int, logger *logrus.Logger) ([]string, error) { + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for %d parties: %w", expectedCount, ctx.Err()) + default: + } + + parties, err := client.GetSession(sessionID) + if err != nil { + logger.WithError(err).Debug("polling parties (will retry)") + time.Sleep(time.Second) + continue + } + + if len(parties) >= expectedCount { + return parties, nil + } + + logger.WithField("registered", len(parties)).Debug("waiting for more parties") + time.Sleep(time.Second) + } +} + +func reshareWithRetry( + vault *vaultType.Vault, + sessionID string, + hexEncKey string, + parties []string, + publicKey string, + isEdDSA bool, + localPartyID string, + threshold int, + oldParties []int, + newParties []int, + relayURL string, + logger *logrus.Logger, +) (string, string, error) { + for attempt := 0; attempt < 3; attempt++ { + pubkey, chainCode, err := reshare( + vault, sessionID, hexEncKey, parties, publicKey, + isEdDSA, localPartyID, threshold, oldParties, newParties, + relayURL, logger, attempt, + ) + if err == nil { + return pubkey, chainCode, nil + } + logger.WithError(err).WithField("attempt", attempt).Error("reshare attempt failed") + } + return "", "", fmt.Errorf("reshare failed after 3 attempts") +} + +func reshare( + vault *vaultType.Vault, + sessionID string, + hexEncKey string, + parties []string, + publicKey string, + isEdDSA bool, + localPartyID string, + threshold int, + oldParties []int, + newParties []int, + relayURL string, + logger *logrus.Logger, + attempt int, +) (string, string, error) { + curveLabel := "ECDSA" + if isEdDSA { + curveLabel = "EdDSA" + } + logger.WithFields(logrus.Fields{ + "curve": curveLabel, + "attempt": attempt, + }).Info("reshare attempt") + + wrapper := mpc.NewWrapper(isEdDSA) + + var keyshareHandle mpc.Handle + if publicKey != "" { + keyshare := findKeyshare(vault, publicKey) + if keyshare == "" { + return "", "", fmt.Errorf("keyshare not found for public key %s", publicKey) + } + keyshareBytes, err := base64.StdEncoding.DecodeString(keyshare) + if err != nil { + return "", "", fmt.Errorf("failed to decode keyshare: %w", err) + } + keyshareHandle, err = wrapper.KeyshareFromBytes(keyshareBytes) + if err != nil { + return "", "", fmt.Errorf("failed to load keyshare: %w", err) + } + defer func() { + freeErr := wrapper.KeyshareFree(keyshareHandle) + if freeErr != nil { + logger.WithError(freeErr).Error("failed to free keyshare handle") + } + }() + } + + setupMsg, err := wrapper.QcSetupMsgNew(keyshareHandle, threshold, parties, oldParties, newParties) + if err != nil { + return "", "", fmt.Errorf("failed to create QC setup message: %w", err) + } + + encrypted, err := mpc.EncryptEncodeSetupMessage(setupMsg, hexEncKey) + if err != nil { + return "", "", fmt.Errorf("failed to encrypt setup message: %w", err) + } + + relayClient := vgrelay.NewRelayClient(relayURL) + setupMsgID := "" + if isEdDSA { + setupMsgID = "eddsa" + } + err = relayClient.UploadSetupMessage(sessionID, setupMsgID, encrypted) + if err != nil { + return "", "", fmt.Errorf("failed to upload setup message: %w", err) + } + logger.Info("setup message uploaded") + + handle, err := wrapper.QcSessionFromSetup(setupMsg, localPartyID, keyshareHandle) + if err != nil { + return "", "", fmt.Errorf("failed to create QC session from setup: %w", err) + } + + err = processQcOutbound(handle, sessionID, hexEncKey, parties, localPartyID, isEdDSA, relayURL, logger) + if err != nil { + logger.WithError(err).Error("initial outbound processing failed") + } + + isInNewCommittee := slices.Contains(parties, localPartyID) + + newPubkey, chainCode, err := processQcInbound( + handle, wrapper, sessionID, hexEncKey, isEdDSA, + localPartyID, isInNewCommittee, parties, relayURL, logger, + ) + return newPubkey, chainCode, err +} + +func findKeyshare(vault *vaultType.Vault, publicKey string) string { + for _, ks := range vault.KeyShares { + if ks.PublicKey == publicKey { + return ks.Keyshare + } + } + return "" +} + +func processQcOutbound( + handle mpc.Handle, + sessionID string, + hexEncKey string, + parties []string, + localPartyID string, + isEdDSA bool, + relayURL string, + logger *logrus.Logger, +) error { + messenger := relay.NewMessenger(relayURL, sessionID, hexEncKey, true, "") + wrapper := mpc.NewWrapper(isEdDSA) + for { + outbound, err := wrapper.QcSessionOutputMessage(handle) + if err != nil { + logger.WithError(err).Error("failed to get output message") + } + if len(outbound) == 0 { + return nil + } + encodedOutbound := base64.StdEncoding.EncodeToString(outbound) + for i := range parties { + receiver, recvErr := wrapper.QcSessionMessageReceiver(handle, outbound, i) + if recvErr != nil { + logger.WithError(recvErr).Error("failed to get message receiver") + } + if receiver == "" { + break + } + sendErr := messenger.Send(localPartyID, receiver, encodedOutbound) + if sendErr != nil { + logger.WithError(sendErr).Error("failed to send message") + } + } + } +} + +func processQcInbound( + handle mpc.Handle, + wrapper *mpc.Wrapper, + sessionID string, + hexEncKey string, + isEdDSA bool, + localPartyID string, + isInNewCommittee bool, + parties []string, + relayURL string, + logger *logrus.Logger, +) (string, string, error) { + processedFirstMsg := false + messageCache := make(map[string]bool) + relayClient := vgrelay.NewRelayClient(relayURL) + start := time.Now() + + for { + if time.Since(start) > 4*time.Minute { + return "", "", fmt.Errorf("reshare timed out after 4 minutes") + } + + messages, err := relayClient.DownloadMessages(sessionID, localPartyID, "") + if err != nil { + logger.WithError(err).Error("failed to download messages") + time.Sleep(100 * time.Millisecond) + continue + } + + for _, message := range messages { + if message.From == localPartyID { + continue + } + + cacheKey := fmt.Sprintf("%s-%s-%s", sessionID, localPartyID, message.Hash) + if messageCache[cacheKey] { + continue + } + + if !processedFirstMsg && message.From != parties[0] { + continue + } + processedFirstMsg = true + + inboundBody, decErr := mpc.DecodeDecryptMessage(message.Body, hexEncKey) + if decErr != nil { + logger.WithError(decErr).Error("failed to decode inbound message") + continue + } + + isFinished, inputErr := wrapper.QcSessionInputMessage(handle, inboundBody) + if inputErr != nil { + logger.WithError(inputErr).Error("failed to apply input message") + continue + } + messageCache[cacheKey] = true + + logger.WithFields(logrus.Fields{ + "hash": message.Hash, + "from": message.From, + "seq": message.SequenceNo, + }).Debug("applied inbound message") + + delErr := relayClient.DeleteMessageFromServer(sessionID, localPartyID, message.Hash, "") + if delErr != nil { + logger.WithError(delErr).Error("failed to delete message from server") + } + + time.Sleep(50 * time.Millisecond) + + outErr := processQcOutbound(handle, sessionID, hexEncKey, parties, localPartyID, isEdDSA, relayURL, logger) + if outErr != nil { + logger.WithError(outErr).Error("failed to process outbound after input") + } + + if isFinished { + logger.Info("reshare finished") + result, finErr := wrapper.QcSessionFinish(handle) + if finErr != nil { + return "", "", fmt.Errorf("failed to finish QC session: %w", finErr) + } + defer func() { + freeErr := wrapper.KeyshareFree(result) + if freeErr != nil { + logger.WithError(freeErr).Error("failed to free result keyshare") + } + }() + + if !isInNewCommittee { + logger.Info("reshare finished but not in new committee") + return "", "", nil + } + + pubkeyBytes, pubErr := wrapper.KeysharePublicKey(result) + if pubErr != nil { + return "", "", fmt.Errorf("failed to get public key: %w", pubErr) + } + encodedPubkey := hex.EncodeToString(pubkeyBytes) + + chainCode := "" + if !isEdDSA { + chainCodeBytes, ccErr := wrapper.KeyshareChainCode(result) + if ccErr != nil { + return "", "", fmt.Errorf("failed to get chain code: %w", ccErr) + } + chainCode = hex.EncodeToString(chainCodeBytes) + } + + return encodedPubkey, chainCode, nil + } + } + + time.Sleep(100 * time.Millisecond) + } +} From da91fab68b809013a307a7d2626d333742072a76 Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:59:41 +0400 Subject: [PATCH 4/7] provide working pipeline --- Makefile | 13 +- cmd/testrunner/main.go | 34 +- config/config.go | 7 + deploy/minikube/rbac.yaml | 5 +- deploy/minikube/worker.yaml | 12 +- deploy/production/cert-manager.yaml | 25 + deploy/production/configmaps.yaml | 19 + deploy/production/ingress.yaml | 49 ++ deploy/production/minio.yaml | 76 +++ deploy/production/namespace.yaml | 15 + deploy/production/postgres.yaml | 75 +++ deploy/production/rbac.yaml | 67 +++ deploy/production/redis.yaml | 51 ++ deploy/production/secrets.yaml | 133 +++++ deploy/production/server.yaml | 88 ++++ deploy/production/worker.yaml | 112 +++++ go.mod | 10 + go.sum | 30 +- internal/api/artifacts.go | 9 +- internal/api/handler.go | 24 +- internal/queue/tasks.go | 3 + internal/testrunner/client.go | 20 + internal/testrunner/evm.go | 90 ---- internal/testrunner/evm_test.go | 47 -- internal/testrunner/fixture.json | 21 +- internal/testrunner/fixtures.go | 31 +- internal/testrunner/jwt.go | 2 + internal/testrunner/mpc/wrappers.go | 67 +++ internal/testrunner/participant.go | 753 +++++++++++++++++----------- internal/testrunner/policy.go | 476 ++++++++++++++++++ internal/testrunner/seeder.go | 196 +------- internal/testrunner/tests.go | 90 +--- internal/worker/artifacts.go | 7 + internal/worker/consumer.go | 42 +- internal/worker/k8s.go | 78 +++ internal/worker/manifests.go | 85 +++- internal/worker/naming.go | 4 + internal/worker/runner.go | 84 +++- scripts/fixture-gen/main.go | 121 +++++ 39 files changed, 2344 insertions(+), 727 deletions(-) create mode 100644 deploy/production/cert-manager.yaml create mode 100644 deploy/production/configmaps.yaml create mode 100644 deploy/production/ingress.yaml create mode 100644 deploy/production/minio.yaml create mode 100644 deploy/production/namespace.yaml create mode 100644 deploy/production/postgres.yaml create mode 100644 deploy/production/rbac.yaml create mode 100644 deploy/production/redis.yaml create mode 100644 deploy/production/secrets.yaml create mode 100644 deploy/production/server.yaml create mode 100644 deploy/production/worker.yaml delete mode 100644 internal/testrunner/evm.go delete mode 100644 internal/testrunner/evm_test.go create mode 100644 internal/testrunner/policy.go create mode 100644 scripts/fixture-gen/main.go diff --git a/Makefile b/Makefile index c26da33..4160c1d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -.PHONY: build-server build-worker build-testrunner sqlc fmt lint docker-testrunner +.PHONY: build-server build-worker build-testrunner sqlc fmt lint \ + docker-testrunner docker-server docker-worker docker-all build-server: go build -o bin/server ./cmd/server @@ -19,4 +20,12 @@ lint: golangci-lint run ./... docker-testrunner: - docker build --build-arg SERVICE=testrunner -t plugin-tests-testrunner:dev . + docker build --platform linux/amd64 --build-arg SERVICE=testrunner -t plugin-tests-testrunner:dev . + +docker-server: + docker build --platform linux/amd64 --build-arg SERVICE=server -t plugin-tests-server:dev . + +docker-worker: + docker build --platform linux/amd64 --build-arg SERVICE=worker -t plugin-tests-worker:dev . + +docker-all: docker-server docker-worker docker-testrunner diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go index 6eea75f..5c96b8e 100644 --- a/cmd/testrunner/main.go +++ b/cmd/testrunner/main.go @@ -85,11 +85,6 @@ func runTest() { logger.WithError(err).Fatal("failed to generate JWT") } - evmFixture, err := testrunner.GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "", 21000, 0) - if err != nil { - logger.WithError(err).Fatal("failed to generate EVM fixture") - } - pluginURL := requireEnv("PLUGIN_ENDPOINT") client := testrunner.NewTestClient(verifierURL) @@ -105,7 +100,7 @@ func runTest() { } logger.Info("verifier is healthy") - suite := testrunner.NewTestSuite(client, pluginCli, fixture, plugins, jwtToken, evmFixture, logger) + suite := testrunner.NewTestSuite(client, pluginCli, fixture, plugins, jwtToken, logger) suite.RunAll() if suite.Failed > 0 { @@ -142,13 +137,18 @@ func runInstall() { logger.WithError(err).Fatal("failed to generate JWT") } + pluginAPIKey := requireEnv("PLUGIN_API_KEY") + testTargetAddress := requireEnv("TEST_TARGET_ADDRESS") + cfg := testrunner.InstallConfig{ - VerifierURL: verifierURL, - RelayURL: relayURL, - JWTToken: jwtToken, - PluginID: pluginID, - Fixture: fixture, - EncryptionSecret: encryptionSecret, + VerifierURL: verifierURL, + RelayURL: relayURL, + JWTToken: jwtToken, + PluginID: pluginID, + PluginAPIKey: pluginAPIKey, + TestTargetAddress: testTargetAddress, + Fixture: fixture, + EncryptionSecret: encryptionSecret, } logger.WithFields(logrus.Fields{ @@ -157,10 +157,18 @@ func runInstall() { "plugin_id": pluginID, }).Info("starting plugin install (MPC reshare)") - err = testrunner.RunInstall(cfg, logger) + reshareResult, err := testrunner.RunInstall(cfg, logger) if err != nil { logger.WithError(err).Fatal("install failed") } + logger.Info("reshare completed successfully") + + logger.Info("starting policy CRUD tests") + err = testrunner.RunPolicyCRUD(cfg, reshareResult, logger) + if err != nil { + logger.WithError(err).Fatal("policy CRUD failed") + } + logger.Info("policy CRUD completed successfully") logger.Info("install completed successfully") } diff --git a/config/config.go b/config/config.go index dff49d4..487bef0 100644 --- a/config/config.go +++ b/config/config.go @@ -69,6 +69,13 @@ type K8sJobConfig struct { JWTSecret string `envconfig:"JWT_SECRET"` PluginEndpoint string `envconfig:"PLUGIN_ENDPOINT"` HostAliases string `envconfig:"HOST_ALIASES"` + PluginAPIKey string `envconfig:"PLUGIN_API_KEY"` + TestTargetAddress string `envconfig:"TEST_TARGET_ADDRESS"` + VaultB64 string `envconfig:"VAULT_B64"` + ServerVaultB64 string `envconfig:"SERVER_VAULT_B64"` + IngressDomain string `envconfig:"INGRESS_DOMAIN"` + TLSSecretName string `envconfig:"TLS_SECRET_NAME"` + SystemNamespace string `envconfig:"SYSTEM_NAMESPACE"` } type JanitorConfig struct { diff --git a/deploy/minikube/rbac.yaml b/deploy/minikube/rbac.yaml index 35c0ec4..99c83df 100644 --- a/deploy/minikube/rbac.yaml +++ b/deploy/minikube/rbac.yaml @@ -21,6 +21,9 @@ rules: - apiGroups: [""] resources: ["services", "configmaps"] verbs: ["create"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["create", "get"] @@ -28,7 +31,7 @@ rules: resources: ["jobs"] verbs: ["create", "get"] - apiGroups: ["networking.k8s.io"] - resources: ["networkpolicies"] + resources: ["networkpolicies", "ingresses"] verbs: ["create"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/deploy/minikube/worker.yaml b/deploy/minikube/worker.yaml index 1f7bbe3..df1665e 100644 --- a/deploy/minikube/worker.yaml +++ b/deploy/minikube/worker.yaml @@ -37,11 +37,21 @@ spec: - name: PLUGIN_TESTS_WORKER_K8S_TEST_IMAGE value: plugin-tests-testrunner:latest - name: PLUGIN_TESTS_WORKER_K8S_ENCRYPTION_SECRET - value: test123 + value: "Saggy@Commotion@Occupier@Registry1" - name: PLUGIN_TESTS_WORKER_K8S_JWT_SECRET value: mysecret - name: PLUGIN_TESTS_WORKER_K8S_PLUGIN_ENDPOINT value: "http://host.minikube.internal:8082" + - name: PLUGIN_TESTS_WORKER_K8S_PLUGIN_API_KEY + value: "localhost-dca-apikey-swap" + - name: PLUGIN_TESTS_WORKER_K8S_TEST_TARGET_ADDRESS + value: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" + - name: PLUGIN_TESTS_WORKER_K8S_VAULT_B64 + value: "" + - name: PLUGIN_TESTS_WORKER_K8S_SERVER_VAULT_B64 + value: "" + - name: PLUGIN_TESTS_WORKER_K8S_INGRESS_DOMAIN + value: "test.plugins.vultisig.com" - name: PLUGIN_TESTS_WORKER_K8S_HOST_ALIASES value: "host.minikube.internal=192.168.65.254" - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_ENDPOINT diff --git a/deploy/production/cert-manager.yaml b/deploy/production/cert-manager.yaml new file mode 100644 index 0000000..a7a5284 --- /dev/null +++ b/deploy/production/cert-manager.yaml @@ -0,0 +1,25 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: wildcard-test-plugins + namespace: plugin-tests-prod +spec: + secretName: wildcard-test-plugins-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "*.test.plugins.vultisig.com" +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: wildcard-test-plugins + namespace: plugin-tests-dev +spec: + secretName: wildcard-test-plugins-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "*.test-dev.plugins.vultisig.com" diff --git a/deploy/production/configmaps.yaml b/deploy/production/configmaps.yaml new file mode 100644 index 0000000..4463510 --- /dev/null +++ b/deploy/production/configmaps.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: minio + namespace: plugin-tests-prod +data: + host: "http://minio:9000" + region: "us-east-1" + bucket: "plugin-tests-artifacts" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: minio + namespace: plugin-tests-dev +data: + host: "http://minio:9000" + region: "us-east-1" + bucket: "plugin-tests-artifacts" diff --git a/deploy/production/ingress.yaml b/deploy/production/ingress.yaml new file mode 100644 index 0000000..2c6902e --- /dev/null +++ b/deploy/production/ingress.yaml @@ -0,0 +1,49 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: server + namespace: plugin-tests-prod + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx + tls: + - hosts: + - tests.plugins.vultisig.com + secretName: server-tls + rules: + - host: tests.plugins.vultisig.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: server + port: + number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: server + namespace: plugin-tests-dev + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx + tls: + - hosts: + - tests-dev.plugins.vultisig.com + secretName: server-tls + rules: + - host: tests-dev.plugins.vultisig.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: server + port: + number: 8080 diff --git a/deploy/production/minio.yaml b/deploy/production/minio.yaml new file mode 100644 index 0000000..a4a91e2 --- /dev/null +++ b/deploy/production/minio.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + namespace: plugin-tests-prod +spec: + replicas: 1 + selector: + matchLabels: + app: minio + template: + metadata: + labels: + app: minio + spec: + containers: + - name: minio + image: minio/minio:latest + command: ["minio", "server", "/data"] + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: minio + key: root-user + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: minio + key: root-password + ports: + - containerPort: 9000 + readinessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: minio-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: minio-data + namespace: plugin-tests-prod +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: do-block-storage + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + namespace: plugin-tests-prod +spec: + selector: + app: minio + ports: + - port: 9000 + targetPort: 9000 diff --git a/deploy/production/namespace.yaml b/deploy/production/namespace.yaml new file mode 100644 index 0000000..110990d --- /dev/null +++ b/deploy/production/namespace.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: plugin-tests-prod + labels: + app.kubernetes.io/managed-by: plugin-tests + environment: prod +--- +apiVersion: v1 +kind: Namespace +metadata: + name: plugin-tests-dev + labels: + app.kubernetes.io/managed-by: plugin-tests + environment: dev diff --git a/deploy/production/postgres.yaml b/deploy/production/postgres.yaml new file mode 100644 index 0000000..23d2481 --- /dev/null +++ b/deploy/production/postgres.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: plugin-tests-prod +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:15 + env: + - name: POSTGRES_USER + value: vultisig + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres + key: password + - name: POSTGRES_DB + value: plugin-tests + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + readinessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 5 + periodSeconds: 3 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: postgres-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data + namespace: plugin-tests-prod +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: do-block-storage + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: plugin-tests-prod +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/deploy/production/rbac.yaml b/deploy/production/rbac.yaml new file mode 100644 index 0000000..8e283a1 --- /dev/null +++ b/deploy/production/rbac.yaml @@ -0,0 +1,67 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: plugin-tests-worker + namespace: plugin-tests-prod +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: plugin-tests-worker + namespace: plugin-tests-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: plugin-tests-worker +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["create", "delete", "get", "list"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + - apiGroups: [""] + resources: ["services", "configmaps"] + verbs: ["create"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["create", "get"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "get"] + - apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies", "ingresses"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: plugin-tests-worker-prod +subjects: + - kind: ServiceAccount + name: plugin-tests-worker + namespace: plugin-tests-prod +roleRef: + kind: ClusterRole + name: plugin-tests-worker + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: plugin-tests-worker-dev +subjects: + - kind: ServiceAccount + name: plugin-tests-worker + namespace: plugin-tests-dev +roleRef: + kind: ClusterRole + name: plugin-tests-worker + apiGroup: rbac.authorization.k8s.io diff --git a/deploy/production/redis.yaml b/deploy/production/redis.yaml new file mode 100644 index 0000000..835f7d9 --- /dev/null +++ b/deploy/production/redis.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: plugin-tests-prod +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7 + command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)"] + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: password + ports: + - containerPort: 6379 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: plugin-tests-prod +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 diff --git a/deploy/production/secrets.yaml b/deploy/production/secrets.yaml new file mode 100644 index 0000000..e0cdd97 --- /dev/null +++ b/deploy/production/secrets.yaml @@ -0,0 +1,133 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres + namespace: plugin-tests-prod +type: Opaque +stringData: + dsn: "CHANGE_ME" + password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres + namespace: plugin-tests-dev +type: Opaque +stringData: + dsn: "CHANGE_ME" + password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: redis + namespace: plugin-tests-prod +type: Opaque +stringData: + uri: "CHANGE_ME" + password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: redis + namespace: plugin-tests-dev +type: Opaque +stringData: + uri: "CHANGE_ME" + password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: minio + namespace: plugin-tests-prod +type: Opaque +stringData: + root-user: "CHANGE_ME" + root-password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: minio + namespace: plugin-tests-dev +type: Opaque +stringData: + root-user: "CHANGE_ME" + root-password: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: encryption + namespace: plugin-tests-prod +type: Opaque +stringData: + secret: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: encryption + namespace: plugin-tests-dev +type: Opaque +stringData: + secret: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: jwt + namespace: plugin-tests-prod +type: Opaque +stringData: + secret: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: jwt + namespace: plugin-tests-dev +type: Opaque +stringData: + secret: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: vault + namespace: plugin-tests-prod +type: Opaque +stringData: + vault-b64: "CHANGE_ME" + server-vault-b64: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: vault + namespace: plugin-tests-dev +type: Opaque +stringData: + vault-b64: "CHANGE_ME" + server-vault-b64: "CHANGE_ME" +--- +apiVersion: v1 +kind: Secret +metadata: + name: ghcr-pull-secret + namespace: plugin-tests-prod +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: CHANGE_ME_BASE64 +--- +apiVersion: v1 +kind: Secret +metadata: + name: ghcr-pull-secret + namespace: plugin-tests-dev +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: CHANGE_ME_BASE64 diff --git a/deploy/production/server.yaml b/deploy/production/server.yaml new file mode 100644 index 0000000..fce0830 --- /dev/null +++ b/deploy/production/server.yaml @@ -0,0 +1,88 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server + namespace: plugin-tests-prod +spec: + replicas: 2 + selector: + matchLabels: + app: server + template: + metadata: + labels: + app: server + spec: + imagePullSecrets: + - name: ghcr-pull-secret + containers: + - name: server + image: ghcr.io/vultisig/plugin-tests/server:latest + command: ["/app/main"] + env: + - name: PLUGIN_TESTS_API_SERVER_HOST + value: "0.0.0.0" + - name: PLUGIN_TESTS_API_SERVER_PORT + value: "8080" + - name: PLUGIN_TESTS_API_DATABASE_DSN + valueFrom: + secretKeyRef: + name: postgres + key: dsn + - name: PLUGIN_TESTS_API_QUEUE_REDIS_URI + valueFrom: + secretKeyRef: + name: redis + key: uri + - name: PLUGIN_TESTS_API_ARTIFACT_S3_ENDPOINT + valueFrom: + configMapKeyRef: + name: minio + key: host + - name: PLUGIN_TESTS_API_ARTIFACT_S3_REGION + valueFrom: + configMapKeyRef: + name: minio + key: region + - name: PLUGIN_TESTS_API_ARTIFACT_S3_ACCESS_KEY + valueFrom: + secretKeyRef: + name: minio + key: root-user + - name: PLUGIN_TESTS_API_ARTIFACT_S3_SECRET_KEY + valueFrom: + secretKeyRef: + name: minio + key: root-password + - name: PLUGIN_TESTS_API_ARTIFACT_S3_BUCKET + valueFrom: + configMapKeyRef: + name: minio + key: bucket + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: server + namespace: plugin-tests-prod +spec: + selector: + app: server + ports: + - port: 8080 + targetPort: 8080 diff --git a/deploy/production/worker.yaml b/deploy/production/worker.yaml new file mode 100644 index 0000000..636f67e --- /dev/null +++ b/deploy/production/worker.yaml @@ -0,0 +1,112 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker + namespace: plugin-tests-prod +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + serviceAccountName: plugin-tests-worker + imagePullSecrets: + - name: ghcr-pull-secret + containers: + - name: worker + image: ghcr.io/vultisig/plugin-tests/worker:latest + command: ["/app/main"] + env: + - name: PLUGIN_TESTS_WORKER_DATABASE_DSN + valueFrom: + secretKeyRef: + name: postgres + key: dsn + - name: PLUGIN_TESTS_WORKER_QUEUE_REDIS_URI + valueFrom: + secretKeyRef: + name: redis + key: uri + - name: PLUGIN_TESTS_WORKER_CONCURRENCY + value: "1" + - name: PLUGIN_TESTS_WORKER_HEALTH_PORT + value: "8081" + - name: PLUGIN_TESTS_WORKER_K8S_VERIFIER_IMAGE + value: ghcr.io/vultisig/plugin-tests/verifier:latest + - name: PLUGIN_TESTS_WORKER_K8S_VERIFIER_WORKER_IMAGE + value: ghcr.io/vultisig/plugin-tests/verifier-worker:latest + - name: PLUGIN_TESTS_WORKER_K8S_TEST_IMAGE + value: ghcr.io/vultisig/plugin-tests/testrunner:latest + - name: PLUGIN_TESTS_WORKER_K8S_IMAGE_PULL_SECRET + value: ghcr-pull-secret + - name: PLUGIN_TESTS_WORKER_K8S_SYSTEM_NAMESPACE + value: plugin-tests-prod + # shared secret between per-run verifier and testrunner + - name: PLUGIN_TESTS_WORKER_K8S_ENCRYPTION_SECRET + valueFrom: + secretKeyRef: + name: encryption + key: secret + - name: PLUGIN_TESTS_WORKER_K8S_JWT_SECRET + valueFrom: + secretKeyRef: + name: jwt + key: secret + - name: PLUGIN_TESTS_WORKER_K8S_VAULT_B64 + valueFrom: + secretKeyRef: + name: vault + key: vault-b64 + - name: PLUGIN_TESTS_WORKER_K8S_SERVER_VAULT_B64 + valueFrom: + secretKeyRef: + name: vault + key: server-vault-b64 + - name: PLUGIN_TESTS_WORKER_K8S_INGRESS_DOMAIN + value: "test.plugins.vultisig.com" + - name: PLUGIN_TESTS_WORKER_K8S_TLS_SECRET_NAME + value: "wildcard-test-plugins-tls" + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_ENDPOINT + valueFrom: + configMapKeyRef: + name: minio + key: host + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_REGION + valueFrom: + configMapKeyRef: + name: minio + key: region + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_ACCESS_KEY + valueFrom: + secretKeyRef: + name: minio + key: root-user + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_SECRET_KEY + valueFrom: + secretKeyRef: + name: minio + key: root-password + - name: PLUGIN_TESTS_WORKER_ARTIFACT_S3_BUCKET + valueFrom: + configMapKeyRef: + name: minio + key: bucket + ports: + - containerPort: 8081 + readinessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi diff --git a/go.mod b/go.mod index 034016d..9c4f8b3 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/bnb-chain/tss-lib/v2 v2.0.2 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect github.com/bytedance/gopkg v0.1.3 // indirect @@ -68,6 +69,7 @@ require ( github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dchest/siphash v1.2.3 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect @@ -79,6 +81,9 @@ require ( github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gcash/bchd v0.21.1 // indirect + github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6 // indirect + github.com/gcash/bchutil v0.0.0-20250514010653-ef9bffba99e1 // indirect github.com/getsentry/sentry-go v0.32.0 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -120,6 +125,10 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/ltcsuite/ltcd v0.23.5 // indirect + github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 // indirect + github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 // indirect + github.com/ltcsuite/ltcd/ltcutil v1.1.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -188,6 +197,7 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + lukechampine.com/blake3 v1.2.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index 86fd6b8..8c38105 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,7 @@ github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bp github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -71,6 +72,7 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 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.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= @@ -80,6 +82,7 @@ github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurT github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 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/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= @@ -181,6 +184,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= @@ -235,6 +240,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gcash/bchd v0.21.1 h1:YTFdypPLIF6vfEyzUXoGCQVg+8JmneZIwb9SYlk5YcE= +github.com/gcash/bchd v0.21.1/go.mod h1:Zco1+b37+qx7xmy+rwFn4XWlbD7PP5z63NBNtrSBfEQ= +github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6 h1:3pZvWJ8MSfWstGrb8Hfh4ZpLyZNcXypcGx2Ju4ZibVM= +github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6/go.mod h1:PpfmXTLfjRp7Tf6v/DCGTRXHz+VFbiRcsoUxi7HvwlQ= +github.com/gcash/bchutil v0.0.0-20250514010653-ef9bffba99e1 h1:AOz9edXkrh8GeXk2j3l4xBSQSyhBszMkDqZqYnwhDPY= +github.com/gcash/bchutil v0.0.0-20250514010653-ef9bffba99e1/go.mod h1:fdua14YuBVIZLlKVXHi8CDrwkvGMGUpGGNvZNgBBv5M= github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -290,8 +301,8 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -343,6 +354,7 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -429,8 +441,10 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa 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= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -454,6 +468,14 @@ 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/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/ltcsuite/ltcd v0.23.5 h1:MFWjmx2hCwxrUu9v0wdIPOSN7PHg9BWQeh+AO4FsVLI= +github.com/ltcsuite/ltcd v0.23.5/go.mod h1:JV6swXR5m0cYFi0VYdQPp3UnMdaDQxaRUCaU1PPjb+g= +github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 h1:HVArUNQGqGaSSoyYkk9qGht74U0/uNhS0n7jV9rkmno= +github.com/ltcsuite/ltcd/btcec/v2 v2.3.2/go.mod h1:T1t5TjbjPnryvlGQ+RpSKGuU8KhjNN7rS5+IznPj1VM= +github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 h1:xuWxvRKxLvOKuS7/Q/7I3tpc3cWAB0+hZpU8YdVqkzg= +github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2/go.mod h1:nkLkAFGhursWf2U68gt61hPieK1I+0m78e+2aevNyD8= +github.com/ltcsuite/ltcd/ltcutil v1.1.3 h1:8AapjCPLIt/wtYe6Odfk1EC2y9mcbpgjyxyCoNjAkFI= +github.com/ltcsuite/ltcd/ltcutil v1.1.3/go.mod h1:z8txd/ohBFrOMBUT70K8iZvHJD/Vc3gzx+6BP6cBxQw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -727,6 +749,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= @@ -931,6 +954,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -948,6 +972,8 @@ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= diff --git a/internal/api/artifacts.go b/internal/api/artifacts.go index 1f537f0..386b135 100644 --- a/internal/api/artifacts.go +++ b/internal/api/artifacts.go @@ -18,9 +18,12 @@ import ( "github.com/vultisig/plugin-tests/config" ) +const maxArtifactReadBytes = 2 << 20 + var allowedArtifacts = map[string]bool{ - "seeder.txt": true, - "test.txt": true, + "seeder.txt": true, + "test.txt": true, + "install.txt": true, } func (s *Server) handleGetArtifact(c echo.Context) error { @@ -80,7 +83,7 @@ func readArtifact(ctx context.Context, cfg config.S3Config, key string) (string, } defer out.Body.Close() - data, err := io.ReadAll(io.LimitReader(out.Body, 2<<20)) + data, err := io.ReadAll(io.LimitReader(out.Body, maxArtifactReadBytes)) if err != nil { return "", fmt.Errorf("failed to read S3 object body: %w", err) } diff --git a/internal/api/handler.go b/internal/api/handler.go index 5b0a18a..c59734a 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -19,10 +19,12 @@ import ( const defaultSuite = "integration" type CreateTestRunRequest struct { - PluginID string `json:"plugin_id"` - ProposalID string `json:"proposal_id,omitempty"` - Version string `json:"version,omitempty"` - RequestedBy string `json:"requested_by"` + PluginID string `json:"plugin_id"` + ProposalID string `json:"proposal_id,omitempty"` + Version string `json:"version,omitempty"` + RequestedBy string `json:"requested_by"` + PluginEndpoint string `json:"plugin_endpoint,omitempty"` + PluginAPIKey string `json:"plugin_api_key,omitempty"` } func (s *Server) handleCreateTestRun(c echo.Context) error { @@ -67,12 +69,14 @@ func (s *Server) handleCreateTestRun(c echo.Context) error { result := types.TestRunFromQuery(run) payload := queue.TestRunPayload{ - RunID: result.ID.String(), - PluginID: req.PluginID, - ProposalID: req.ProposalID, - Version: req.Version, - Suite: defaultSuite, - RequestedBy: req.RequestedBy, + RunID: result.ID.String(), + PluginID: req.PluginID, + ProposalID: req.ProposalID, + Version: req.Version, + Suite: defaultSuite, + RequestedBy: req.RequestedBy, + PluginEndpoint: strings.TrimSpace(req.PluginEndpoint), + PluginAPIKey: strings.TrimSpace(req.PluginAPIKey), } _, err = s.producer.EnqueueTestRun(payload) diff --git a/internal/queue/tasks.go b/internal/queue/tasks.go index b7ba03b..3f560ab 100644 --- a/internal/queue/tasks.go +++ b/internal/queue/tasks.go @@ -12,4 +12,7 @@ type TestRunPayload struct { ArtifactRef string `json:"artifact_ref,omitempty"` Suite string `json:"suite"` RequestedBy string `json:"requested_by"` + + PluginEndpoint string `json:"plugin_endpoint,omitempty"` + PluginAPIKey string `json:"plugin_api_key,omitempty"` } diff --git a/internal/testrunner/client.go b/internal/testrunner/client.go index 2c757a9..2dd8d91 100644 --- a/internal/testrunner/client.go +++ b/internal/testrunner/client.go @@ -73,6 +73,26 @@ func (c *TestClient) POST(path string, body interface{}) (*http.Response, error) return c.httpClient.Do(req) } +func (c *TestClient) DELETE(path string, body interface{}) (*http.Response, error) { + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(jsonBody) + } + + req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, bodyReader) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + c.setAuthHeaders(req) + return c.httpClient.Do(req) +} + func (c *TestClient) setAuthHeaders(req *http.Request) { if c.apiKey != "" { req.Header.Set("Authorization", "Bearer "+c.apiKey) diff --git a/internal/testrunner/evm.go b/internal/testrunner/evm.go deleted file mode 100644 index da93848..0000000 --- a/internal/testrunner/evm.go +++ /dev/null @@ -1,90 +0,0 @@ -package testrunner - -import ( - "crypto/sha256" - "encoding/base64" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" -) - -type EVMFixture struct { - TxB64 string - MsgB64 string - MsgSHA256B64 string -} - -type dynamicFeeTxWithoutSignature struct { - ChainID *big.Int - Nonce uint64 - GasTipCap *big.Int - GasFeeCap *big.Int - Gas uint64 - To *common.Address `rlp:"nil"` - Value *big.Int - Data []byte - AccessList ethtypes.AccessList -} - -func GenerateEVMFixture(chainID int64, to string, valueWei string, gas, nonce uint64) (*EVMFixture, error) { - chainIDBig := big.NewInt(chainID) - toAddr := common.HexToAddress(to) - - value := new(big.Int) - if valueWei != "" { - _, ok := value.SetString(valueWei, 10) - if !ok { - return nil, fmt.Errorf("invalid value: %q is not a valid base-10 integer", valueWei) - } - } else { - value.SetInt64(1000000000000000) - } - - tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ - ChainID: chainIDBig, - Nonce: nonce, - GasTipCap: big.NewInt(1000000000), - GasFeeCap: big.NewInt(20000000000), - Gas: gas, - To: &toAddr, - Value: value, - Data: nil, - }) - - unsignedTx := dynamicFeeTxWithoutSignature{ - ChainID: chainIDBig, - Nonce: nonce, - GasTipCap: big.NewInt(1000000000), - GasFeeCap: big.NewInt(20000000000), - Gas: gas, - To: &toAddr, - Value: value, - Data: nil, - AccessList: ethtypes.AccessList{}, - } - - txBytes, err := rlp.EncodeToBytes(unsignedTx) - if err != nil { - return nil, fmt.Errorf("failed to RLP encode: %w", err) - } - - typedTxBytes := append([]byte{byte(ethtypes.DynamicFeeTxType)}, txBytes...) - txB64 := base64.StdEncoding.EncodeToString(typedTxBytes) - - signer := ethtypes.LatestSignerForChainID(chainIDBig) - hash := signer.Hash(tx) - msgBytes := hash.Bytes() - msgB64 := base64.StdEncoding.EncodeToString(msgBytes) - - msgSha256 := sha256.Sum256(msgBytes) - msgSha256B64 := base64.StdEncoding.EncodeToString(msgSha256[:]) - - return &EVMFixture{ - TxB64: txB64, - MsgB64: msgB64, - MsgSHA256B64: msgSha256B64, - }, nil -} diff --git a/internal/testrunner/evm_test.go b/internal/testrunner/evm_test.go deleted file mode 100644 index 3924440..0000000 --- a/internal/testrunner/evm_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package testrunner - -import ( - "encoding/base64" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateEVMFixture_HappyPath(t *testing.T) { - fixture, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "", 21000, 0) - require.NoError(t, err) - - assert.NotEmpty(t, fixture.TxB64) - assert.NotEmpty(t, fixture.MsgB64) - assert.NotEmpty(t, fixture.MsgSHA256B64) - - txBytes, err := base64.StdEncoding.DecodeString(fixture.TxB64) - require.NoError(t, err) - assert.Equal(t, byte(0x02), txBytes[0]) - - msgBytes, err := base64.StdEncoding.DecodeString(fixture.MsgB64) - require.NoError(t, err) - assert.Len(t, msgBytes, 32) - - hashBytes, err := base64.StdEncoding.DecodeString(fixture.MsgSHA256B64) - require.NoError(t, err) - assert.Len(t, hashBytes, 32) -} - -func TestGenerateEVMFixture_Deterministic(t *testing.T) { - f1, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "1000000", 21000, 5) - require.NoError(t, err) - - f2, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "1000000", 21000, 5) - require.NoError(t, err) - - assert.Equal(t, f1.TxB64, f2.TxB64) - assert.Equal(t, f1.MsgB64, f2.MsgB64) - assert.Equal(t, f1.MsgSHA256B64, f2.MsgSHA256B64) -} - -func TestGenerateEVMFixture_InvalidValue(t *testing.T) { - _, err := GenerateEVMFixture(1, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "not-a-number", 21000, 0) - assert.Error(t, err) -} diff --git a/internal/testrunner/fixture.json b/internal/testrunner/fixture.json index 7d0f39c..65d54bf 100644 --- a/internal/testrunner/fixture.json +++ b/internal/testrunner/fixture.json @@ -1,20 +1,21 @@ { - "vault": { - "public_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "name": "integration-test-vault", - "created_at": "2025-12-22T00:00:00Z", - "vault_b64": "CAEStANVeks5NXNMb1k0OWxyTjdzUHprekRQQndtdEx3UktWelFUajFsc3BUbTI3VVM3OG9lS3ZKR0djRmQ1RGViZVlYbzRpR3M3d2dXRFBnaEYySms4NWZ4bW9GZTU4ZWp6UjV5S1BVcnFZM0h5di9ydVN4SE5tVENmT2NLbElMVFBodGVpckFOaTZBMFFNeTYrakFqL3JrY0FQTFZ3cmFTS1h6VG84ZUxoTHE0aXZKQWM3Mkt1Yld1Rmw1YjFMUndaUURVOVQxK1A0YnVWT3NGNGNTK1pBaWZhQWppOG9UbHFxR2Vwb1NyQUdabGJOa2FWWFYrbFJmclY5STZjaFlsaHZ4c0RvR0NxdUZabTZiUEw4NVo0QWN0U1A0N0I2SVFOK0RFYlAvaWF4dVZXY1hqMkxJV2hiUnZZQTJ6OHRCWjhyRWF4U3BqWk1DK1pyd0dXbGxrd3I4WWFkLy9RRVF4WFQ1UWFxb0FmVjZMcmphT096dUhwck4yRmJJbjdWRWozdlFYOWZsSmZTVXBMZ3hwdHdUcmQxWXV6cHlMVXVRZ25ReXRhTlRyZEt3c0t4WXNDdzRNdz09GAE=" - }, "reshare": { - "session_id": "00000000-0000-0000-0000-000000000000", - "hex_encryption_key": "0000000000000000000000000000000000000000000000000000000000000000", + "email": "integration@test.example.com", "hex_chain_code": "0000000000000000000000000000000000000000000000000000000000000000", - "local_party_id": "verifier-test-party", + "hex_encryption_key": "0000000000000000000000000000000000000000000000000000000000000000", + "local_party_id": "integration-test-party", "old_parties": [ "party1", "party2" ], "old_reshare_prefix": "integration-test", - "email": "integration@test.example.com" + "session_id": "00000000-0000-0000-0000-000000000000" + }, + "vault": { + "created_at": "2026-02-20T13:31:32Z", + "name": "integration-test-vault", + "public_key": "02695d42f955f01b1f4037d2e2e0674d8d13f401b0a475453f2b265fc90cffd5c5", + "vault_b64": "CAESkJsHeDZlNUMxcThpb3VZcXFiT3VvUjVLSzNJYVVqZ0RLTzBDTEQyT3lKbDBvYktadmJGT3JrZGlDN0ErM2lFMXZXbFZLTTZCRGt5OVlCUUl3ZkNaQmdVNTNoQ1U3RFpOY1dGcEhSd1NjMDRoYzdQeWNLRGZpMEdQMklHZnQwZFQ0dFFGcGFyK1VCYm5iUkJqT2lRb0ZKNHBqSUc2SnMxM1gydDBOdjB1WTk1TVdMdFFscGJJL3pVWUFYaXpXUDNMV3hDa296VTRhaHkxZ1VnUjVHNmU5QXpnRlZDQ05pdHM2aHg3dkdrSDNtWDBCQUFsQ0oxLzNNSGl3S2FDU3NVeVR4SUJ4V2h3TFVSWmV2czd1SjErQjROU3NBa3plQ20reEFQOHRzOEdFWmtmQXBSQlVkOGQrbEdxNlFnSUtkNjlodEgxbHRDbHVQak5QNHpaSlU5aGRldU1zQkJxNXF1cllZeWNFOW1tbTQrT1NnZGg0VWtQVGJoRVVTRmFkWm5vV3VlYlF1eklvSXhMdEJDL2FoLythZ2h3WDd1T1dSYnUvbjNSTDAvY0hXWXc2M2htU0ErRUswOTUybG40eE1OMnFVa1J3ZkwrdHNzeHZVWUtyNXJYcU8weG1GcW1saGhBUFgzR1gzSmdwS1g0czFDbEs5ZHVBd2swTjhYYzZwQllpWjZFMG1sbXlNR2VTNGU0SUZ4V2RRNlkvUS9EczdXa2NBSm5UV2ZtdzVwRks0OUxRR0k2K253WTRNU1FlcER1eGU5ZkhjS3pqUUplRThnVVliTjYzSXBMUmpJalR1QzZHbVZnVlYwU2w4T25sQUVTTkMycEZQSmwwNHc2clN1MXVZcVU4MzZLQXFJNDFZcDNVUXlkMFF2YU9vTktWb0JMNzNNbXcyalpJSE1JWmt4RHNlOVpyb3pYMlF0c29tdnUrMk1FamxzbEpaYVhWV0ZnR2hSRFVNRStWYjFEY0gyaWNad2FnMlNWd1EvK1IvWVQzcFRNaW13UUxmVUlhcGpwUlFiSk4wa3psWjFITFJrbE9SOE5JSnRDTFNEbHZyL0plWXdGZG8yTHZaSmlQQWtzUEFmZVBiUFduS1djdERBTzBFN1dOOW9UMlZmdm9kdlRxeU1lblgraHRJbWt1QVF5eS9qNWw4LzZpaXV4TmN3MXlHU1hZTlhvdG9xQVViNkVkVTYyV1dudTk2dFpRWDlKRkoreUZpMTA3U0dMOFZkbVFyNFg1SFh2MTRvdTBWaWNTaVdqeG1ZWkovRzhZZmgybjNwbnBhZ1lydUxRWWFZcFNzMXpuczgvVEJmWjdZb1Urc1VNZ1dsNlR2RFpGbGxCQWlvSE5IdWRVR0ovM3hNeDJYMlg5eGhscXlqVUZ2SW0xbjJpVUNHWEtOaXNCL09DT29MRDJ5akdPY1ZXRmNkOUthdDJ4NWR2bWR4Yjk0NElNR2NPRjc1MW1uek5VT3A0elJaOTFZME5ReEx4VEI0a3B1eW85VTRWampYRUl0R21MSWNCcWZlVjBWbERFRjRXSXFNdytPenU2SW15c0VwM3YvVkEwN2JCV3crb1VPb3FuMUMvQ2Zmckl5cEhZTElKLzRzNXZ4QjFQOFJRSFJWUDl0OTkvZ0hocysyRm1pcEhYV3kvM3ExcnJxbUY3aVlSdS9aNkhPTUs0c3psK0FvTjlTR0o2djhNQ21XVWc4U0hLcHcwaGtnSFFLcmc0b2tjYXNvVDd2QmgzMFhGVmFaTzVBdVBSdFhNam9ESFFubm1MQ3RiWmhldkRpWklrQmpWbDk2WUpCSFlCR2RBeU1sS0JCMDBYODZYSHZhNHNsNFg2MkRKQjZSekxvdGRHY1Fra2phMWZGU0g3NjJ4SUg4SWxFQ1d2THh0V0JlWW16c2NqK3oxV28zdERhVDBPU3dDZHd6L1F0N1hLUGtYS21zYk5sdkppMEw4K1VtRGVBRFVseDFDNU8raVBjWnpIY0MyWWxkUTRPWHpLUzlwZzQ3SnR1R3dVRHNpdmpSakIzNGFhajE2Ly9qQnFidHdPUFYvOGo0NFZzRkpUWVRyUk1mSHVqczd0VVduVU03cE5nc2htTG9QUW5PdXpvaTVhQjNrNFg3dXBYazhUNVJiR25Ddm9IbzBCcU1PN2dxYThUMmdrOUswYTRRSDRkNjgrd05RWUdkUXpvVC9OTnc3UE5pN244SEhpZmZ6WkN3RCt4OXZ3U2VqdjN2eGNRYWFKT2ZHU0JERlEwdGFwTXNjcFozakpSUkx1bnNlZkVsSG9oVyt1NFJsR2wyVG9SWnB4NXcwalNXempWM3hmWGl3OHI4M3l3Z0EwNnNhS2FaQllDWG10STk0VWZtdWQ2ZlhZV1NZZitmMGJ5VzBFWXFrQzN4VEhaazRTOXcvWkFZM1pQM1BrbmxvZjZJaHBqcUdySWExNENPbkw4NlRKREdUN09kaUtwK3h4YVdVcW9ySVFHMmk5dFhzc29jRnR5YTdRTUttMlJENjlMVXJsbUQ1enNNN3kvZEFiTFVFYnlrNWtFVnltRVNUd2wwN3RGNFdFekdqdFIxMGlYbktDbEtDemJWd0x0czJzWEhyZk5zTk1MZnpWZSs3WXlwOHJiMmFCSmhTU3RkWnV2Sy9WYmZrb2JqNGovMU5uVVVzZ3lHZm5pcndSWWt1dFZVWkkwazc0YnNLVEx2VGVLT0JsQkh1eHdSQWVnSm55ZHA3VVBqMEFJVnRGOC9wMVFHZkxDeVlaZDZlSUJMNlhUSTVSaVhXOTdFeUxwaUZrNGNUTU1zL3lzRHlvTUFPZ1h2SSsrMkdnNkIyamUwVCtEYVJEOTh5MlNselpFVWJCSEVIYUJuUkIyQkZUK1lDMy9aZ2M2N09GNzdyeWsyRitCczBrTmVaZ2ZZZVppcTdESENyQWFML3grb01lM0FlNnVyeUxCUm9zZlVpamdidVF1dlJqMkJVdkx4SXFaVWZPeG1XekhXcHpBUGoxb0U1SmJWQmNyT3RoNHhBaWRpM3VJOGtuTzhPWE54bHhZQWVGdW04MTNPWkM4MlFINnk4bHV6Z2U2bzhmNTgwRkp5UFBIaUVIL292THVzQ2dWcnFBaUdCZDZadE5EY1Q5enNFRm5vQ0FDbWtHejdsaXRHbnF3cUlmQXozS3RmaHdBeXk4OTRGbEYrZFJuRUkvWVI1UDUrTXd5dEtHWXNnYy9WZy8xb0FTR2U5NVJUTlBveS90TG4vOWV6bDdyKzdRTFM1WElseXJOOHgrdXd3bDUzNWZJZzdKREJUdHVFOFhWTXFEb01DOXliK0pZTHVmVHRsRkNkRnFLemlVdFBJWG0xZXdBQTlZbURRam4zVmdSZW9iS0Q3L1hibjNpVmpSOFZXWk00eVdTVTVwMytLbWNjSlBrZkRrS2FyOHhCa1ZraUhUNVRnN3JPbGQ3VS9ZREJLc2FOaExlQlJiNncyS2l2WHRlTFJvbDBGaFBaWGJneURGT1ZvRWRtcXB4LzhwR0NldlJjZWxodnVQMWliQTNqUk5YYk5IMy9wNndMM1hpbEJtM3NXVFBYdWwvVEI0VmliOUxDVnc5TWN4cXJjRjlRalJIRWM5WHZxUXlwRE82UW52SlozZUtOQ1pKT0xEVWU1ZXJ6cEN2T0lXa3h1WFpGZ0Y1RGJuZkRHTTRqVGFzZ0I0RDJQREVHcmQrM2hJZzFqaXNIN1Q5TGd6WVdSbW1FZjBCZW1EczNJN2lFSDFPK2VnZFVkamxXZTREQ2F1dzZ3aEErMGRVakJiUUtFRTlWS2pqKzM5c1BtMTN4Njc4WnplQ01FdEVySXBqMmV0cnJkYlRncFpvODdyaDZ2QWJjMUxPdTJXbUdhRjlNQkhvWGQzZS9CWnlTRVd0SlB6WXEwYmd2Q2k3U3owMVR6T3pNMktYN2xQSkZ5SHlVN1BZWHFxSmM2OUNLL2MwcENUZko1M29pUTg1TGQ1Q3hFendSbWl4NmtHTVVTRVkzQStkYWMzL1ljblRtVytHSlpQUWkwSDVLd0xyUUd3d1pVaHpLSWg0MU9GL3dJaHBSSk1mL0JmVTM4ZXJKY0szbHRmQzg1a2c3d0FLQ0lWcHd3UjU4WDc3S0wxemc4VWlzVXlISVQzczMvaDl3ZlhuQTJtc1Z5bno5NlNpNkEwSzFzSkp3SDA2b2d6NUN6Njg5WkpMRllMeFR6SG5MSFdKNWpKbGNmbU43c0dxUHZSQVMwdDVrZjdTMDRXdkhacVNvNXNoM2p1THAwTUZZRkl4eFdleVNzTEF4Tkt6bmVuUS9LWkpyNENQVmVxOU9RYnV5R1dEbnJ4a3kvTm4rZldVd3dSQU9zeDV2Y0tuWk9DS0VTYmhJOWtKYXBEa1VQQlFRM2M5TDR0WDd2UXlyZ3pSVHAwYUpYMDBUcS9ZOWNBOW1HZ1ZwaTZhQXhBREw0UW5pSDIwTGIyVkJxUnB5cUJVc1dQbm1Vb2xGS3B6Z3ZneXJRODFzSDFaL1F3RkJXclhWK1RQa25GYWFPZXpGcld2cStIektQdFd1dHJPNXA1WEhGRGNpNHZ5TkNFNXhCUXpDcm00TmVZa1lPbEY1WnZoWXF3MGoyOGRlNFVoZ3FRZE1xbE9YblFQRXFFSXdLSDcrMzU4TVhCOFpXVzRNQVNoUWxTcG1RUnBSR1pibXVkS1RkM1c4MjRQTHMyNWVkV1NZejl0bHplTzF4T3lNVTYwYzlIU2kvTWZtQXk0Vk13czA2aWRzeDU1eDJOaldmQUFDdlVjb21FRW1xandSQ0E0dWZJT3NrYW4wQ0ZkQXp1cVhORjQzNHhIZ0dkbThTZWY4QXFMNFBEVmo0Qzg3ZXo2SGRzQ1ozWlNNLzNWVm5qcEg5NmZzTzZLSGJJR2MvblYvaUFVODU0VzJUNDY3bklCNFhhVHNudUVjUXJkMFd2bTcvYXZzamZybitDVzVxdjgvUmU2VEFDZjAyRVdBeWY4STI5eWZEeVR3NVFXQkpVZ01OZVBHMnR6U3drOXBzRWFOR0tzencvZ2hWT0IybEcwS2FPYUNqUk5RdjBZQXZVU2UrUmVmbkcwOHBmbUtOZDBJSHgrSitUa2xnV2ZEWk9SRnU2UHIwVER2ZFhvYnRJenQ1cytOSXJmT1BIYWM4NUcveUVxbFVBTHRuYjNGSnBoZ0xnNkdNWklHQk8vOHlVQXJVdWJ3WGJBcTBiMkVJNU9sWDkzbEczYjluUjkySzJOZjl5ZkVEVUwzKzM3SzNyU2pMbE5ybGdNWEFHamowUTZNNEhodUpCeXRBcXZ3WmdPZlVRc2t2dnNjSmc2OGJuK2dnMzJJdW5QVkVISmhHeTFtbzdVZCt5STY0Ni84MGpVeGxQajVUaEIwTEw0NTU2VVVCektsTy9jQlRwdm1oS3o5YVRhKzMwbXRndS9tNWRNdzZqWXVrTG1uMG5jamZibW9SMmJHMzRGYzNQZFBHaDdMcmowVFdoVG8wZzZ0UWJyRnlWOWRMY1ExdWVLQy9rK3pMa2ZwODFIQlpMMDJrVVQvaHFVN0R2OGlHZXFPUElGVUNabmtXZHl6M2RBVjdHaDV2L1c1ckEvV0pXRmxDUm9jWDRVamVXVHVFbzFBYk9SdFlseXN4MFoySGMzelZwRGlvMG53bm5iQlpwdVpFcEVjTE9LS0NjRmJIM2VXM0ZGZjR1YmljdGEyV1ZBbWVRc3BTWmJ5Z0x1WmVBaUoyakNZYkNIdXJKZ1JLSjNzS3FneGpPelBVdXMwWWl6WFVBQ3NubzR1NXpmdTlrbkJPTnlsN1hCTGFiQjJMMmZQeDlsMW8wWlNoUGVQek9kM01lYVlaVnRIT0czQ2R6WGNmV2hCeWVpeWd5Q1k3QmVHalZ2OUNGS3JWcmZXNFhVK1pabUF2WGNrV1hoUFdrcmNJNVpYQkFhTTZTeHVscnAvQUM0QitTWUQyY2Z5azlUOUxGbXhvNUEwSzdFYit3YWcyY2YvOEZGQTNBcFhFNHVWVEtKMU5HN0FFVmYzaWV1NTNMdXpvU0pSQk5kSC9IVDVjcW9qeVZPcWEvbEQvaDZrMEpqUk14UGtjanJlWjlkbHF6cjNKc3QzTGZ2a0t2SEZWNVBRcXd6U2pidlJJMWtBUndkNzIyRWJaRmozdi9pcUJEMmRSTmUvWEF5bkFxRGcxbkgvbnVQYjg2ditldVBjeWVHMVF5WXRrdzJKdGpQV3FVbGMxUmhEdUNxQ09ZM21wSDV1Z1VTVm9iVGdKODdXU2ZGN2ZnYms2K3BmZkhrd1dCMlRsWDZUOW01dGZkV25ZcVBzZG1xMWNnSlU2QWR1MmI3alJwTFMyQTg5a2dJSWw1djd1L0pEM2taUzBmeGwwWDNtc2VkejNZVVVhMlh2UURVT0hmRHN3ajNqdjlCTVl2V3l4WGJZZkQ3R2RBdFpBTTdpMG8yR0VyRE1iazkzVGp6RzFhMzZ2TjFVeEFTb3F1eEkxYXZCcmdhUWpsTnZDcVdJYlQvdzFvRlJYakozd09ONG5VTVA3dHBFbzhYbnZyejUyaC9LMU1EK25JZFZOTXNCSDZKTE5QamNWUVFCeDh4ZVRMR2VSdFg1Q29rNm5Eamw3bG1pZTA1Znd3Z1ZlVDFETDd5TGJQNUxpZGxZQTJua2RCeHhKMHdJeFRobWlYZEJBM2Q3TXJsVi9ON3JLZlg0Sm44ajFSVWtadzRtVWRtT3JNY0pNNDkwYWVQS0kwalJhNlNOdk1XUklmNDcvQjN6UHdoWnN3WUg4aXFkTnJla0J3akdYUm1aNWMzWVZOb3k2bkRDczdqL0UvcU5CcUtZMnVMMG1OZTREcmowSFlPU0thbW1mT0JxMk9XUGJkSWR3a1RoaTZ4bGo5Z2FDcWdBL20xays2U1NQYU1oeVVQQWhJR2VwVGlERE9aaTZGV21IWmFVdTFVeC90cE5mOGJaY2JEclNqTUV1Qkg4eXYrbm9wbWp6Vm9ZT0xpaWFrUExkb3F6eHh4MHMyNHE0cC9lZG00b2hUQjNXdnNUaUN6cDYxbk9teTlNdk90Snd3a2g2bXpBRTVvQ3VpbFdSVklZNzREbHpxT1EvaSswM1RReWVmcFBScGE3RVRVTlR5Z0lvMmwxMjA4WHhnSFNneUVRU2RYSkhrY21pY0dwaWVIWjFXZzh1bDNrQzF4Z3JvTGRhNCtudFBLcUk2MUo1QnhSdmtnb0RkaUhBQ2ZIaEZuUEZ3K21mSFN6NkgwMitzOVhIMFp3KzQ0cEEwLzNDdElsVStCZm1PSDFqTmRyWjNTcy95OVhwZUpDeWMwRmtzYTA2Q0o3TjhoQ1NlSEEwQkpEbUpZL0dlOEoxbkhuc0JMKzQ4Q1dWYitKNEdMM1lSa2Ivb3lnemptMUxUSVNSM21oZUpFczJOeStXa1RXc1daVjJ3SEF1RFlsakxUamtndFFiMlVCOG9uY2xKTXN6bWswU3h4azAwSjJQaXd0WHdwdDhja3JhYXY4ZHhFMk5Qd2R6YklKdjZNOUh0SXZYeFZRUVlyeE90WVkvU1h0WXdTTFVFU2x0RWlEcDM3dUtVUWtLRjZaay8rRVBucy9SZHBneGFYOXRwOGpsWko1b0dSeE1ZSFJJSFN1WnF2SFBBRGZuMUZmV2U2TnBtcm94S2hHNnpnRzlxT21saUcvNkQwejlCQ2hMY0NtU05yQ2dWR1U5dGlGR3lhczJGUE1UdEFHUzNNRjVWdURDeVdKVm8rOUtuYTBudWNiSFNSRzVnbmZ1MWYrZC9VRWdIYWd6NFV6N3dQSnUwem1iK1JLSHV3WmRGVVhtcVBaY0pVNGhiMWw4ZW5LTzNEbW5xaGgzOFluR2luUzMyUFAyMFhmMzBnUnNjamt4dXBpZ2lTRXZ2TDJkcGlRTmN4eHRjaG5sSlRQZ1hYbG1jUGhBcHdEemxTbzduV1RyUTNpRUVmeVVPMXpCWEJjWEkxZmVZQlF2VTIzNGYxRE5wZXBIQkdYWjYrczhod2lpMU5qMkZ6b0lKNjB5enRaUVRQcGltT04yVGtuZXI3K2t6Ny9IRXdMN2VqZFVEbThqYVNBZ0hxaC9nTXI1ZkZVUmI3U041NC9SUUVSMjIyb0pPdjBUb3NmRnBMa0kxZHUzQXdVcFN6T0t6ZHRlMWxsNDVjS0Q4L3l1ZTByaVlmYzRxalpBTTlKS1N2WTFWdnJjTDM2SzRIdmZTVm5yejI0Z1p3ay9UWWJ3RVpMWmYvWlFKTHd5ODNFM1FxMXpacnFLa3BPN01nMFlYRUVGMEthVW5uTEhzNk8wNVhVaUs3Qzh3TWFoeGhKblJtelhGY0hhRXVPSTdkL2tZcExpOEJQMXY3ZVA5Q2huUVNZSVF6WndrRmNEUDI4akI4a0U0b3lCRkM0eVBJWHc1Y2d1MUlXUU5hNEJyb1dhUm1TR25TMDRlSFN5SmJobEszSnFJSW9XMUdtalVnMG5VbHJ1b3o4YjVFNkJiem1hcEVCRzlSak5iYUM2RmFydHNubmM1WDhHUWsyZVg3V0paWkVoWmtWdEthUE9tdnFvYU9XQy9iNjdwdDVLcDJlV0R4OWhHeHByd2tQWVdnMFVRNHJ1SFJjUzB3T090YWlFbEhteU9XNlIxT1ZzMjRVOFRrcXFqcU5pRGh3blVQS1dROXprQjFpeEFMUGI3S0p3aEdCK3VRQkVpV1ZxREpyOGxpYVR2bndTWGROc0JiWGxSZHR5WktpejltZFNnWGdZUFVlc1lQcG9oY0lvR0J5d3laU3VFcCtObGtGT0RTb0JRNWQrRDhmSTQxQjA1cFpuUVFCZ0llVUNBQVdBUkdEK2FUUThuZEhxWkRwa0k2eVAySXFFUm5NWjk2dWM1emk4M3hrNXBqaEVXVEdhaHliYXBNaHVUV1JSNHcwSUFHYklYUGFhL0tlLzhyT1ZraExCZ1g3OFNTQ1NlZ2RiRldkcENYcGsxVWVZSFNEUFhGc3RuY1Vqc0lzcS90L1VrNnRBT2pPL0NGdnV3Rm5Xb3IzWjdTajZRM1NVaVk0MGs0QjJ1TE10alZ3cWg2eHRMLzVhaTFJMnpaREpNbXJSYzdLM01zWGsyRFltRkF1VTM0Zjk0b0F1eXBXclRRUjNHejJlM1djaEZsU2pEWHVoNG5NMWxmak9HWmpsLytTZFNnMmY5UXd2V1pwWGNLZjVscWRsYzRDdVZha1c1Z0ZxS3pxcmlIUTc0UHh5WmZiQWYzdGVVK1dwMGNWS2dGaDhLMGNMRTRvYVBWOTdCMFJWc014VVFCMkZqYVZGVWthckNOYnBDOU5GekFaKzEwNmdERVZwOVN1YmZpb2xORFM3Zks2WVlqcnpqQXk4L3pWZklGN0lPbmdlbnlZZW9LL1oyZzFSejNFOVgvTTF5bU9VTC9GWUpOUjhyWFRESUxtb1pFZm1NT3pzYVV6dWlrVEpwdTd4UmZvWVBJOEhqS3lhblBwVXN0NklGVDMvM0ErZnJEQUtOYjhGSjV5cjNJNXJoTnYrYU45OWhZT3ZYemhFU0RHZGdzaFh4WHZnTXRoS1I4MVFNZ2xIOGxTdzN2M3hsYXQrVGk5VnF3OFdLUkRWU0p6eCtVV1hQNGd5YTNuYlpwWlRTaHYwK1h6YVk3VlRPcmFzaUtIQzJMOWIwTkZYVjI2QWU1WE9sWFZDQTd0NWxaUEltZS9jWkRUZXlrU3RReGxXNjJ5OTBQbE4rdzIzZzh1Mi83amdyOTdPa2VzQ3J5d1drM3lMUXMwMmFUYW9yQkZpYWdqQUgzRGNBa3kvckpRVFJUZFR0QWtrckpXQTZRM1ZvSnZreWhJSDVCOXN0ZDlYRHlRZU9vYTZ2dGlINzVxRVRKc3lWclc1TGRRQ2QxU3pmY0NzS3cwQjFmeTNJVW5XY3FEU3NYSGpNTU9sc1QrbU5kQUlpUFllNDBwaGJUdEx3SjNNOSt3WTZXL0tiUzEyRVNRZmNFWHY4ZmY5REhnMGNrZmdNR0hjdzV4YXlQOEtzNzBiaVhSNEtaV3Ryc3NPL1djMFJKUlllVTRmRlZsNzNwS21zeEI4WXBrR1MzSWZwZEZlWDNtd0pvYzZzenpRTVZCUG1DTFhsWEs0YnNNNjBUcms4alhwSktQSXpVZE1FbGJuTzNwL0RDaml6UEpzaVBNUjhqaUVaaStLZTdyM0Zpa2V2bmhiSURHaXNlbXJ2ak80WlRpUENiMCtQeldNd2czUmp1ck5nU1N2S2ZYUmJRWm1DOGcvdVE1d0hRclE4OXQwL1NnWmxOWk5HMXlSTU9uZmZMb2JEeU9jemhpU3R2RG9QeFZHY0lhaEszSHdoVmZHQU9PU2hmbWpOU2hmRDlpK3VRNzM4N0dxbm1PLzNmTEVlNzFrajJHK1hZZUJZTnNRc2J1ZVF4aTBPd2wwN3l2Mkw0d1IwT3g2T0xLdXJoZWMrc1h2VlFiTHE2TXloZXE0RkhrNCsxTDNuVWhRZEZKcm1yOGJkVlFjYnVpY2p2T3ZMMjVwVzJMZXFJOFpBL2ZzbVZCYWFTeEZUUmZsMUcxbllrb2N2cTVQQVRlQTZaYUF4WkNzNmxObWtuN3NUZmM4eXRDMDA4Qm5vd0pjSHdrZGpxK05tZWJDQVFlVFJBMXQ0MFc1Q2s4OGNXNUMyQ0hudDQ1WVFveERjaFBoMlhabExRWitBY1lDSyt5V0pvQWg1TGloWGtGdklWQ2p5bGFmSTBXSFQ5NDlhcEUzY1l3ZC9OSjBkdVFlSFN4QlhwSDlNeVlNNDBER2NVRHk1eVBiT3E4OFF2L0R5dFVhNU0vRXpOMU5KeFg1RDRXME9XZjI4dTh4Nkc5eDgxdzBTMzhpWmFMYnlTbzdVL2hsUi9TeDlVWlFxZlVBT0hvZ1ZWUVZGZjZBQUJCRXZoMVpNYVRQWTlIT1JPK2ZxVmtPZnVCd2NsRCtja1Q1UkZFbzlTMVVvVEljRnVMU2NMWWxHMk1lZ0g2eUpvckxpbUtxSjJoVVZFdVdUbE5MUjRMNFlJSVZpVExZeEtMQXNiYURqbXN4NDdkSUN4aHhNeDJhb3dqUW1PYkdDZDNJTjQ3cUNrMzI4RE9FRmNOWlIvSVFyNmJBT1llU2xxUC9mTy9FOGRXaExQdlk0KzNmYy9QNEcrVTdSandPVmtvYS92QXpkUmEyYnpCU1k2YUg0akVEQXNvVnAzL2VUWEpXWVpFOWkyT0sxL0lLTDFmTU85d25vZmx4OW9EUDZrQzE2VnRSVitkK05tQ0E3Zjl6ODRDMXB6dXkxcUJOSldBYkNObHg1d1g5UjNHSjVBZXEvb0pFWVB1V0cyQ0xUNTcyNTBwbzBEOFlXMlY1Z2RJYXFQZXlPM0NtOUpOOUxDSmJrSC9mN1pyWHNqYk83Tm4vZ3hsNFFhVDZFN3BVR1FyM3FWcE1MNkNHKytOK1IyU1lkUjhmcC9ZckNJd1Z1RnA1elB6U0RuZUtkMUNKbXh5NTE2VjhRV2V2N29LUC9oRWZOaUcrTU5jSThYUEtZa2VZWVZzQ0VmeXBIV2hvallLdlB2b2RPQXlxNS9naGNpR25mS29NczNCQ0l2cWRrL3hKcC85TmZUOVRLMmVkdUZMSmc1b0xSaGY0aThycGorM3ZEUkNXNG54UU5iNC83OTVNQWFoVE1WN0ZZWmtUOTF5NWUxeHNGNjUxcDg1TU05bVlzc1pITDBicHhyQjlaUFcrbFU1MG9ETEpPaE5BTWxwbytWSEoxS2lTTUx0Y25kNmtxNnBPRlVDbXBLQjhwV3hvRnRWWmM5NGRwK0llQlJGZlBGSisyZGg5Ly9sdFhLSi8rKzFpNy9VdndCZ01wYWVhSmtkSGVDT3pxUHdTdFR3RlVuNVlpcXEyS0RnbVJkdzNGZGUwczhqQ0ZyV3lZQ25LeXRmZk55MkV1NW1rK2dJWVc1QmRLNHNRelN4QnhJN3RWbGk2dFFhb2Z3SkNYRWNIeENIN0xIRVN0NGVpc2RvLzQzTWduTTBIYjVWc0ZBQUpwS2xZUDRHWmFEcDkwUms0V0YxcEVTK24vOGpXMUZvbDlqb05LRnRjeEVLN2JqRVE3TGJISkNBL09IWVh4RTlMWDB5eW45UFdkejRzeDhoRFo3bU1WRVp0WmtOTlplMmZzSXBSK1hNckJPeWNpZytIQURiRWI5ZVV0V0ZiL3RNMEROUE83dVZjSzhtRXdrcXJSZi95dVYrcHBoNHNQTGlGeG1YeEx0ZGZITHRaRkJCa05udEY3NkZ4dDAyM21qY3FtdkxZYlgzajJCRVRxdVBUaWdmKzdEd1RZZW9YbDZXVm5wUWhiUVJpWExtbWxxZnZlYXEwWnhBQ1FJS0NIZDJJcEFzMnZZbFdkaTZXVXdsNUxQcytpR1hJQjI2STI2MWJCU3NLaitWQW1XRnVpWUgySkhsZ2F1QyszSXdVVEx0WllDT0VRVjYzRkNHaFFZMzBxUGdQMllZVFpSMU0ydUQ3RnJ0MnBIajNDZ3AxMVRJNnNhMlhWdDVPSUhBdFhCYjNhdk5nV3pnZzRhUHkzTXlPR0c0QStjQlhDZUo3ZTRjSmxta0ZWQ0ZydGRaNEFWUlZRa1hxYnEwV1o5V0RXRGlaQ0ZyZjZzUkZ5ZVk3bFprSjdXdTFQcjh0T1ozaXZGWm5ZSzJyQXVaejAwOG44dC9samVyZEJXSDBMMmppdTJlb202Y0E2a1gvTTMxZWl3THRPdFozbXdudkVOcThyVWpoNEV4SzRmQUMxVHF3OTE3b21uRm0zUEZoWnFueHQ4cGRKckxBa3NRT29BY1VydjNabVI5RHU1c0NUcXZhbUZub2hrOTAwaXZtak9Nb2hkWi9aSXY5Ly9kcmdTcCs4cEphdGJuZHJDRzYyMFE2dWRYbUxsQVRsZ2tuMzFXZEZ4MWowYWszWkhnTmtkWVZmcjJ5b1g4Vk1OeThBMlNxaHd4YUdydG5OZkxyT052dEl3c0Z1UTYwcTY1WjlvOFBPS2JkWFIwMHZmd0xjZzRaeGE5MGV4dkxMSDBiKzBjN0UyeVpZMXE2ZUdjT3ZBaWJNUjlZTjBkd2FET1Y3eG0xVm9pWFFTSUFseFg1TVNxby93NkM5ay9RZlZFS0pRdTZzKzJjZGdHb1pYYkxZb3RheWpxMEgzZ1V2LzlaR0llNUZzUkNxemlYTzEvajBzcHhkeGdWQmJya2FpZ2NIaTdyTDhLR1Q5L3hpbGpBR09Qbkt6KzZJNm5oVzZWbXR5Q3VRdTEvY1dQamorY0NFSmZTNURvTmNuSm1VOXpueWdtS21Jb2ZlQSszS2U3UXdpaE1TbUhZYkFORGJQbW5EaldNOFVFVVJES2huaTdDRnRNK2x4eFNlTHFZMDZjVjEwTEY1RGg4ZkhhTVFHcDNpR3VXWkRsMWx3bXArOVVJT08wbDllcnRhYzF1cjFnUjV3K0dqZWhkM2F1RkZjbDk2NE41UXJMNmwyaGtoT08rZ3hiaGMvb1lFM0dPdVlUaXg5K1dQRWd6eEs4OEJkdzBuVzVwbXNpRkY1MHk3M095bVRFNjl2NE1GSUprN2tsMjIvT3ZNekFTYTdmbXJlcDZrdnFjQWtadzg0ZzBpazlPdHoyYW9DNWVWOTRKd042NHZqd1RGYjdjc3p6b2RvZFJ3bzRMY25IN0xvOFErSW80ZERVeUN3S1RZNStuaGEvKzErUHZOWkZsYm0wQWVjODdxQ1BpQzBiMmdkY3NHT0tibHlwdm81bW12ZmxpZG9hTDB2cWg5SUViQ2NvZUlMOCtHUVpjSzNJc3VxTlNzL1hFWUlBMFNDcjcxdmZuekJ2MlZSUUdBRU45Y1hUVEhtUnU4Y3c4eGlwU1AzYm9adzRDeE1veFBRWWc2eWNJUzNzQVVxMEZ3UG5Iakh1STc3S09CbFpFb0p6WENaenBaMFZJNUcvNGhSNFhCbWZOZE1lMzJwZ2xRT3ZORlkxMVMxcVJQaG1pRmtkWjdOSDNzWXNMWWJDZGorOEp0cmJvQVUxamtZbHJ4UWNrQ1ZhTWFtakxHcEQ2bW1UQS9vaGprcnpSV2Y1NFU2RHh5aEErc2VGQ1FlZGVHTkkxVENQOWRpQzJqRXMvZWtMYTd1a2NTNGJ6UTMzWGpBVFkxT0FqLzZRZ3RoVFVHUmdnTmQzQVFLSm4xaUJQblp2aFI5U0ZFTWJ3enNIaW5LTGJ2Sjh6NTU3dkREbmRjdHk4TW1ZVFArdUZCdEFwNlduamhRNDJBYnBXYWVzR2lsVEIrL1VNZzZKYkVyV3ErZ1pFeC9hRlY4enlCVnVGbHBPbHVqSUdNSVk2by9ZN1ZTV1ppTjl3eUF3L2FURTkrZjA4bFNnVXhsc29uWVFnY0lzT1BnTU9pbCtieDIrM0JFeXhvalF6ZEE0MENVQ2YvWmd4clRQUjlnTTdVdUxjaHZrUC9pK1BBcUFLRzZKZmF5R1hGeFZJdlNFQ1AyZytzV2V0cm41UEd5UnlUaGRaekFnUG45eGE4aW5Oc2hVSGlRam5JMm0yWjg0T2hUUldlTlpzWWJiSjR4NTRTWGtrcUt1cCt6WllPakljOUhrd1IxRnN2Yi9ZZkFCWTl1NUtKaHpWd1BuZHZCL0cwRGNmN3pFaHpTWDNMbjIwZWlQajBWUnF4WU4ySUQ2cWtHOTRXRWcvQmlIandZTXFYYVlGREJoMVArNmdrMTZ6TXRFV3d0YmpRRlY5V3VZZDhlSTl2WnpVNUlTRWVyVW1CTkYwd1E0OElWQTlKanhuL1pZek9laDVJN0ZYVGFuQnYyVUk3aDRHZnJYQlJQWU5mYVZZR0NZU0x0dDlVcU1mOXY3NlBiajRsNzJrcVh3NEp1TWxibTg2dkNNMmx4WHhWNUJwSnlBMXlEbUpFMFh1WkhZNThYVnZReEtjVjRmV0lOcUthZEdyaUVsNUtaWEUyLzlvSVRiQ1o1NUd3L3RSdlVpVmU5a0owdHY0VDFGNGZTSVNsTnVTVWdGUjZ6eUZVQ2VxV0V2SmlKYUpxRDZTQlRKRERKcjRPS3BZRmxPZmI5SE1KbVJUNTYyT1dHVHZBclZiNCttenM1UjdISG5kdTdOU2pQNFNMMTZBanFFcnJLc25zUTFGeTFxSmR3dGZqUFFZNUxOQlkxYzJQWHN4eEM2NVlvREhkcWxtZUxXR25qWGgxN2wwR2ZYT0IwaXBZZjZuUzRVc2F1cUhLcE4rYWhSYTdqK0NBRVZFaFhudko4enk0MVF2YXFFYWlabTZneUxUemZCcHV0K2lDaGY3enRCZFozSHRQR2JHdjltK1VSdE90SUJDU0hTWTZMQUV0NGliNEI0VXZ2ZjRMWXRvSUcyM0JSOEQzRHFzUlQ5bHVybHhKeDlWVGN2QnNxNE92SzBIS1N4U3JLbG1GblZOdytBY0RHZzg3N3BnekV3YThrZ25sa2NxU1VldEZXS3NUOEcwQmJJRFhxNndsS1dpK1VLSVcyeWM2M3hFbnFIRWdWV3NGZWxtWUc2cEtadEFiUXoyd2pWTzFWUkdSYzMwckZ6aU5ZUUM2ZlMyaHpuQU1wODNLcmFaTmR6bDRSOHVwMjZsVDlyeW5KMUNXdmQvV3pkMkx3dXA4UG1nUFNGZnByU1V1NUF6SituV09CWHZHR3dyc3o5aFZHcnhWKy9ZOGk2NjM5QWFzZGE2bldwbjhpaFozdWtYRWNvNGZIRUtPQ09OV0hRMU8veml2OWx0alFlTU51TlFTTFhIeHp1K1IzQkRMZ21ZOE16VzFEMkxObHJlYWEyNmF0R25UaEJHTlNrTUhmbnpMZDNUKzZNNHRYd1JhYUdYdjlLb1RBSFRBVkJjY0lNWExUUitDVnBvalY3NmMwdmMxdm4rYzh0R0huVVlBbUxLbGJzSVVuNU45MmF3QjhJOU90d1piNW9GYW1iTkcxNUhvcWV1MEIyZWJnc0dpYnJPanFIWmtPS0FxMmduNXJuMkhRTS82UzBENWw1a2dyUUFaejVEeERwa2IxanB0RThZSHAzMGlkRFpEWGhBU0hPc0l4OEFRTERqVVdUbWFFLzNmWGhMUWQyYmszalRGZGtJOHBNMys3djFlMW1mbmZWUW1LQ3pJVnBLaWNieWlVbzQ5dG1SMDc1S1d5T3ZCR1ZReVl0VEZIUFgzQm5LdVNrRzNrQTVIa29aeHo0ZnUxdk1BQnFQVWlWSExXd0YxdjFKc1dSSjRkOWpHeXI3VG1BMForS1l6M1Yxem1GSmEvdVpuZkxNMTJHeWpuY1FGNHE0WG11NjJQOG82T3JjVWZrV1BnbUZwYzhqcEdVSWtYMmQ0ck1xWFkweHJjWFl3VEhuT3FwcnRUKzdpWGwramtmaDRqcEZIb1Z1MVFkUFRBS1BWVVBGbkM4b3BHbTJLT3BjNVhOaUhaTFBycnIyeUZ5ZVJQMWdNZTkrRk9QZHlxUzJ3TmJzVVdJdW9FYlN3MWNBUDdUUTVVckdMcnU1T2lWM1JuSGhnVVZXd0IvWUplOTBmUURzZnFMcXZLRlZ0WnBsT2M5cmY3YXNCSC9qWUpscnMvV3RLR0hWREhMS2tKZWE0UlBSOFFsRy9VTTRlUlkraTB0d0pHNWZIREJjQms0NzFNOTNhNGhhRHltN3EwSk1mU1NTQlpSb3pXSmJSbUJEL2NOT3VlOENFQ0IzT3FjN0N0L1plY3hVRUppU0pZN0FudHcveUZGbmxuekRRWjFaWC9UOWNyVGl5ZFBaVS9Qckk3RGs3ejBRaUh1azRyMTVndzNaQTcwNWZUL3FwdTFxRVk5dFZ3V2JldVNLeVlwSllXYWJNeXpzL3JEMEJDalMzNlZ2Q3p1bG41T1pwY1RSaitCbloxeXlxbnBoc3VWZmZoM1JYaWRoTmpiazBIMWhqK3Y4a3VVRGFOcm16MTROQktxbHc4N2FkbmhDTCtud09wa0tieVJzOU52cDNUSWUrNHlxOXd6MVZzdUdiVzRlZEJiWmxhL09iQkRKVWdhM3RXSFRobTBuQVB4UFBJYnlubHA5Y1ZIdTZiRnlJUHdpWHBwaDZtdlNETTcvVm1PU2YvR1l2VEhYUXlzL05HaDRubHJvUVh0UjhKVzlNQWFCVVV0dGN5YVVoYXRVZkUvQ1lyWnpoMkRObnZ6cTE2SlJQcGtHVTdTbXNneDVVT3czT2tlMU9wQmp1NisxRm5hOWlNNlRvczdOS2VyRUhyK1VrRVBQMnBmU3RPTEhKTnNic2dkWkd2bjl4YmdCaFdWZkxkQndTTXJkbnE2SjdJaUIyd3diVGVjWDNuTk9Pa1BaTUwwWEt6Wmlmdjh6bzVlTnJoTiswZGgzWm02VVdqSGE3dWpTbm1TcXNYNWRJU01UaUR2YU1VUEYwOHp6cTZ0SmJtOStOb1BvN0dlQlc0ZlhUY0daN3ZRS0pjUmF5TEsrajVUVDFEWHU4MlMySU80dlZKeDl1UVl3blhMOTRXNmk4Wm9waWRqMXFVZFJnVU56VHk2RjQ3V2lMWkY3MVR0SGRBS0RzcGUyUWpqQXh5SmxKL3RSeXlsNWwrclVrTEkvL0FDcTNtYnRpdndxZ0tQZ0plQ3hOaFIwUGo0M2ZsaCtzaWZIOVJQMVZ4QS9tNDdpQmlIRzUxZXJXYUVabXJEcmZ5ZDBONVlMKzQ2ejNIZHV0MFlpbC9QSVRCL2VKSWVOOExha1hIakd0TEZTRXdxNVlqWFFUTlViZDdWSEhPdVVTNHgyMXF2dlUxQVlGOE5rMWcwR242S0VGTHlxV285M3Y4TFZYWllVaHBwV2tWYW1tQ05ZWjBJdkpxUmw1UE5qc1lJRUFRTy85VmVDVll5U0JJZmlCUXVSRHZoT24vZmMvNlM4N3pUYzJnd3UvL0dKWnppY0txdVhIM0p5UUxsOHF5VEhoSy9zN0RGd1MxYUpTQVJaVG9iZnNxKzVicXFkWkoxOGxKWjVvSE8xMjdqTXlveVVaK1U3L3V1UzR5Z3lneVRCeHQ0bzk3alpML3ZpRGc3T0FGSVorcktPZmNlSFJXMWsrUUdvTUNCWGtsbmttSzRyM2UzZ3NJbFV1dGI0RDJZcm52ZWpNT01qOWNhT2xrcG9ZTTlXV2hRUzdlTU9rbnpnMDNqaFJDbGsxVkJUbTRQUmRCbGRjWGhFQSsyTmV2SkpzNlRFcURYT2NsZm4zenhsaWg0NU03OGR1cVFPdlA4WFJRaDRucFRKSE9xZWRnYnNLWURmTnh6TFlJWENxV1lCMXVNdG9wYURCTUxKMWlxRkdialoza1lsdiswbWgyY0tjSWdHcTB4cXJwd3VhME9tNXBJNHk2a1o0ZWdZNDdrUzJDR1NQVXYyNDhKTmVoZ2pTOVYvelpmcVF2Z3hnWTJFRWFOclZMOFJEQ0hZL1BEdDFBbkNxZituMWI3czhtLzZRVkxaUTl1d0tVNXN1bjNGOWJUaDhJanRSdStGaGxvejJXWG1RUEt0RXY5eThocHJrQmZuZHp2RkJrek1OejIzSVdqVXY1allGTTkyOXRmZHB0K0FiKzhmaFpmdmlFbkdwNTdTY3lVVnpDa1EvR1ptSHNQNmNoL0dJWDRZQWhyd29HdUp5cGpSbGVnVmpVMDM0ZHlrRm9rVmJYbTdOckRGYkhheHpoNzROK2tOa3hZMVFRTFJYOTJBRGo0aHcvWUpHck5VdU1DMkNoUnl1MWNKMHNLdSt0R2tENlNPQXpUdHo2ZkZZUWpiQWljS2lrU3JWUjQ0aTY4SVlDWnNOSDdFaEN0dWZrOC91NEpNQTJwNFRuN1lBYWYrb3htc1dFVWpKd3cwZFFEVTR4VFk5Vk1vbExsUVU4VVFlZEJwSnI0VHhoUHVrWXUrcDJ3clFaT1BkZjF0VXpHSlovZGVoazdYSUhPSkNzbFpUdnBXQTV5RkJiUjFseUxkTEo3dzU4SFM0Q0NLRzFkSlRCUldETTZEUjF5VkpTcDJGTmZ0OVVReHZvWFE3Z0kxcGFsZW5zYUx5RFNYcVdxWno3Z2ZDZWdwNjFTZE5jTjdraGNRL3FUUEVqbWw4OHZlQkRSRzVBbnZhV1JjeUZKSTdGZzBGcW9qTW9DZFdXZDQxcHpSZTJhK085dXNTc3RNczgrMXMyK2dIaTZndm9tbU1ZSGtQZGRLaHpCenV4MDJ1Znd4ZzkreTJtbDJVWnkxam9vRThreXVkWEQyQlBPb3NsdEk1VVBtalc4ZGFuWmxtMndRVmVFcUJFcVJCOVhZWDVVeDNOVlo2bFhGY1lUYjRNelVJaXIxWDlkVlVXNjN4TWJnb2QrQWtxTVpQUGtQNjdTWnF0TU9DamZjRGlKOTYyZm5JeGRXVjZLQTJOWGhNQS9pQXkwRE1ob1hJR1BnbnV4US9SeEFTY2Y0Z1lzcXRMRW9OSUpSNld1Qkl2Mzk2cjJXaGhGb0J4UGZKTFkvTU9vMDRnU2VYNkh4aW0vMVc3dEx6ZEdSd3RpVnEyVjlpbDJSbTFBZUJpVXRaM2dqTkszOXc2eUZZOVpYdlJSVHg5ZGVpbys3bHcyWDM3SjNZTm5pWTc5eUlVa2pRaFBCUUFMZkY0R1ZVTGdwSlVCTXI2YW05dis3VmZBV2sxRlVSTDZ6WVoyaXIxMmkyc0NhYkFjZEN0enVlQTgwQVpTL0R4WDRrcVBnODcvMjFkekpqN01WUk00SG5DNFR6SWxwWnQ0enVZWTFSbXEyenhrejd6MXV6SWtZTkkybGN1ZFhiNTVLYUlCdC85L0pXQ2tOZUFxZ3BlVk9rdHA5SXdQWC9WYUgwK2Q5MHl6VW1uQlE4eGQvbTlUWjlnMkpPdUFqTUFiTWpHZTF0SjdadW5PRXd2aExES3dTNnd6VE5PNHlaVXJuOFgxU0NXaVNRdGEvQXorQ3RpT2x0Tmk2NG9wTXN6OXlEZXhFRjhOc2ZXVmJ4anNXNDRZekdpRXk4ZmpwRzh5VXR5K0hjZUVFRFFXdmRaTmZ3TVBLaTZnSjZVbmZaREtEOFpxbkU3ODVvdTVBRi9hQjl3T2FLQ3V3bkZBOGdSa0t1aVFtYTU2NTBiUkxXaE8yQVVDUG9OTjRUUnpEdVlqbko5dGlyaHdzd0Q2eUp1ZlA0YmFpOC95RjhCOEFyRE0vUy9aS0xHUFg3dTRPeDFtQzQvSWlVSzUwdWFmckE1T1ljczNXR3M5Znl2VDJrWHIvUFdCVlI4WTZEREErMStsRi9ERVkvSFVwYWRiNFZXMW9HaUh1K1cvMTZvc0dZTis5cGlrVERaNkRJUW0wd2J2QUhrV0lnNUM0azlKalZRQnBjRE9GZU9mNzJzZkkyMy9RYWlBKy9EbGtRT25WTEdYYTA1YkRQQkE1OVZtVzlOWmpQZi9LcHhDcWQxU3hlRkluTkdYVGUyZXNVSXFnb2VobkVCUDNvenBnYXpOd2lUVExadTZRblBkelBCNWdwWEFTUlA3dlltZUFlVWdFM0xqdDhoazNCRE5veWNZWHErVjdBMDBmSjZWWnRpTFRXVy9MUHBaRlFmWEpGcXZjaFNrczkzdUFwRWJRek9WK2ZpeW1HeHB3VW5kMTRZNjNuUUt3UkJpR2dRS1ZzZXFVRE82QTVHMDJmZm1HckdSWjdWZVpCeHJzMnpMREdsUDFiaWM3bzg1dGFrODJDem9UQ2VLc1JZWXg4NDFScytCNHFTM0pBWlV2SndmOHNMNlFYOXNZdlF6SHpPSzhLYWVhbStoVjIxVkd0MFNqWDZhcXdRaGlBdEMybkZITVUyNzFmV3lpWXRhb3gzdC9vVG9kVE9GTFhjZXNUc0VleWs3OCtpa1JJc1crQmlvdk0zZGt0UDZ5bjdVdmh0a1dqWlQ5Sjkrb3R0djlhcnpTamJnZzZ4Z0R4UTAyeDllZEowUkMyZ3h2V0xLcFhSSXNtMXg3bTE2K1RRSm5zbEtudmpvQ2NWN1k4SHFKMkgrOWpmdnh5eVNMbk5IMnk1eDJ0aFpTdUp4OUJHQnAyelFtUUFrRUp6eTVJdTF1MUVVTjVzUFdzZE5tT1gwZmxWVTRRV0xieEFLTCs4a0tUQ0hTSXZkRklqTk9sWHhkOHZUYklZUTg4QldRdTZQSW1pNmo0NkxzVkI4SFFuaHdkM21CMEJUOEJPZkNZdXVOM0FrSFVYNThmSmJ2amR1Zld6UzliazB6Wi9WOXkxMlBWM2s0dEZ2dGE1dG8vM3dkZ0NNRUVSVmdCRkVmdzZBS0x1aTZhZW96U1Q4TGhZWHRqNnpmSTNUbE1DNFlVcVdtdnhxbHJtL0d6UFloY29LV0ZEdGJCUkhQTFhnWWdjaXNmNGNNb0tqMW9Ueld4R0t5TWdqR0RiMFEzTTZwWTkzRThHaThqaWlVR2dMRmxqaWNOVG1XUStML0Z3SGZaMVVKYnI0RkdoaHhFSUFZR2pyYWErbCs0M2dzRnVKRXp3SUN6OHowbTBlc0xPSHE4UllCN0ZWUzhUSmNHRHBNbTZjandEYmdRTWJwdnRNKzRjUncwQzZROGlVcXRQYndGaUZGVk9BRXBsbUxnSHhuanhpSDYrYUltaFA3OVBUalhzYWdxZ2dJN2F5UVVEazYyMnRDQzZuM2pqY2dmWVhWeG1QMXNHbDNyWjJGYS9wWHRPampSaWhlazhlWjJVcmFwSzBHTkV1Vmc4TlNheTJmQ0N6ZUd2YTVReTNIeXppbUt6emxZZnZSK1l2UXc3TmFWQnFVOVNiR09RQXJEK1Q2Vkw3S2pBS3dsbEZpejlWWlZnYWN5dTFrZWNiUEVlSXVoSEhiNWhqYUFwM2h4bkNLS3VXTXpGdGRReEo3MHhsZWM0RUhQR2lmeEQ5Rllla05KaTJTc0FDMG41NERzb214MzdzZERDREZ6bnR0TGE1RFozQndrQ29VY0ZtdjgvMk1lcjVvWlRDL1BRL240ZDB0SnpNTm5jc05EdTRmNW9naHlCMEtIWThxVnF6Uyt4M3VGNGx6aVRnZHJYTkxNVXM0R1VROG5rR1laQnQ1K1JsanBhNDQxN09WOC8xZEJWMnczYlhlZnh0OXZNWjVIcGpJOE1qUVFvd0MyM1VWbWFDRFFjQTl3bUc2SkNNZDBQSEo2eVYxQi81a0QrT2RWbDRuQithYk9PNyttTlpZaWVVOEVFcnl3c0FhWi9ySkFneEJpbEZHcW1ndFFUL0VjamhIU1VYSXBCV1V5TXJZbWxHNDZRWTZkRUdjUzcrQjlCVUdvOWxEQzNtWHFmU0RaeXloNC9MaFpiZkNqS1RrdVZQVEVCL2svQkZWQ2ZldkJvR0M5ZWpyTjBwOXp5RnJaTTk5WWh4Uk9mWGgrdFkzaGVhQ2hGVE1QZ2FraGhBT090N0VHdHhJVmtaZGVVTXozRFJyY21iOVNNMFJJTjNINEI2ejNOM1UzSjVsLzlkTDFZdzZhdWU0U1c0clB3SG5KYk5SUWJESlBwdTF5Q254NVNqTzNFZUFYSk90TnBNeGxTY1RlenNnbnJ0RDAwVzBBeU9mZkwzMWloVTZOWDZNeFI0a0JWdElnc3hHUEZ0d3BBS1RpYXV5amFINnphMldHMkxJaUF1UGpBYU14blpPVEZUR280cjU3Zjd1N2RPbHdLb0lVWVlpdElrNElvNHZvMDlCRndKYUdkaFUxRWJWK0E2djY5RDZYTjJKTktLcWFpZ3FJMTZTTTlUZ1o5eWpHQ3hqZllLSnUwYWlZWE9CWTFaSGd4YUpuSEk3ZWYrU2VRclpIS0p2M2Z5a2tRR041bjJvNUo3NHlSUVJhUXhkREx4OVpGRVQxR3lMRnkxRG1kS2Zibk1kMjMzcU1CdUNHTW9HR0ZBRVRrdjRTRG5sQUxnZm13RW4wQ0t2ZTVzQ3BZeHNpNmlMU082RGZubGZuUnhsdER4ck43T1VTZTVPWktkTDZaMEZOYVd6R3BDUTJrc3BiV202T1p0OUpZUFBubSt6bEptc3dpb1JMRVBuS2kxQjRsZS9HM05XeHdRWWRQYW1xWW02L3pvanByQmUrZHdaSmJpV0hYTlNzRHBWMFRuNG9mekd5b1lTVWhxR0F4WTJ3aTJFMUlpVmlqUjFyeEFpSmNobGFBeDF5cEhISU9uQ251VVp3SnE5bEFGZlhqeFNGOHpRZ1VCRWowaEthWjRxamJiRWU0STMvNWlBUExONHdsYVpFbWhlbTROcFlNR2pmNjlmSnUyN2kzSkt2ZldjZWtmMjgrUTZqRHBsS0crcjRoSFpraG45OStySGM2Umc4ZG5tWGUwU2ZGZ3cxY2NVdEs2OC9SNFVlR1ZoMXlVZ1F5RkQ5Y0J0VnNxTDdyZjJCczNLUHp4NTc0OERCODVHWENFU204bzdvbzlGS3Vva09EMW1ROFhSdUFuamk4NlROcFpyazBzYm1hV3FUMVdjYmxQc0FuMG4wSWtLRWRvUmhHMHZlcjg4ektpY0dmWldZL1RqaXE0OExpcjRuWGZWSmR5VndSMmJDVnZEclI0cWZWUVpGLzk2Q1laV2FTU0s3ckNoRDVNcU5ueFZ6RjNJVkoraWVVQTlZYWhvK3Uwa0NBYVhwdlJqTGljN2VxZWFVSms4dUZralhwbmRqcEQ4eXMxaUFoTDc0UE42K0lQeFNkME5KeTdIVFFPeXVLdWdWWHY1VFQ3b2ZRVjlmbDFNUmgzVUtBekJBWVAzSWNhak45ZHI1bDVmT3ZWNCt1aHV0V3dBZVJKN2tNRHFWYWl4N25QUXJ5NzRESS95S2tlcGErM09sWVZaVGxmWUcvN0Yyd3VaOWZuRkgvc1djOXQ5ajV4a1NzNFo2bUxQcGJwb29sL282TElPK2VLL3l4TXZpSWFBOUxFWXhiMmpnUWpTY3lNOTk0ZmZqUExBcEhLYndmWmNYaFpvMU10YkRUY2FVM2pEVXRzbVFCenRxaGltMDBOS09DekZCZlBJS1FJbkNyRTZBZDQvWHdFKzhhbnpnMmlZY0tlRHRIVmQ4K3NNSDBUNHRpYVBsNkJaWDJVYWVIRjhZVmh6VDZXNWg0VWw4SVlqV2hBREhlU1llQjV3WlZaUXN1M1lXdXNDL1pGclhwLy85ODFwb1BINW9TMHorZGFyRlRzaGZRT0Z5d1V5aEE4RjVPcXRjOTc4dzVwbCtpWFlUUDhhRTFsUFJkMFgxa3NUeWhDeVpzZGZ4RU5JQ0JFdldLcUdCUWh0ZWlQMFJKQXRZZGk1Z1pBbTJ2MStmZ3p0VUl5SDNld0ZGWXBrL1A5eGMwdFpDUElJUldKNlRjclhKUzBmM3c3QjJ3YmQ5UlNZdE95ZUJKS0w3Um9EOTROUHZ6WE1ocnhmWjV3ZmxqVHlKakxvMHB6UWdyTTZRQytibkd0S2o0Ums1cDkydXB4ZDRRb1RocnJEZ3NVQmsxTzRRNXlpTjFBb1N4aTF3d2NhVkYyWHFFU1dFMUwvSTVJV1h2dmkwNVpNZHNxb1ZRRjNXU1dpNytxZCtPY3I1R0NHUmRaM1JzcGY0cDBwTDM3SDNCMWEyZ2tUK3NVV3l0ODBLVU93TjcyZUk5UlBjOUpaM2tmQ012Y1NLckIxMG02QjRvS3hCL0N0MG4wbHBPRXY0Zktsc3FEdmMyeXFXMUt6aFVvQnhsNExleHBkMFh5U241VzdDN09uaDFOTWxRNWp4eko1V1ord3Vmem5FUVdmalRCMklsR2dCQ3B5eHc3UGRiQ2UxdjhIT2tzaE9QYys2WkdaSDMwWWlWYXhEeEYyaFlleUViMGpTdG5EbnJrVTZxSWZ2VHpvWEVXL1I3djY1V1RBTGd5Mndveml4eWN1VWRhdTRlS3dtcklVaHZoMDhETDZxbFNqRnBzNmtPOU1YNGZMVFB6TElwSmc3U0VPZDR4Sm1Tdkl4TUJIaHZvdXRkWjUxTXpYQ0N5cnFzaVNGN0ppRWFZUGFTd0RUaXlSYnV0RHczYkhhVVlwdzdxUklFWE1FSVhiOENXTUlKTUdaMDd4eUI3YVZRalRqTFhSZzY4alYyOERmbUh6WklScUQ5ajBpT2hRMHpEYzZnejdlY1Nvd1VzTHFsc2pKMXFYL3VSRHZNaTI3RFNZUWFpVXVJWUFIUnI5a1R4eGhUU1hyNnJZNUs2RHpGOHNvRENrMVc4c2tSdVQ1Q252TXJONE83cmthUmhtVzY1NGpzZStqUmU0V2dQOHdMYjArMGt1SVBoOEIvNXFiRDd5c3p3T1lEOXFmZWJwTXRRd1FSaGR0UjN4VkR5bE9KSEN5UGRuNjZVdG9IWmlONkpVWjY5Z25FVGhkZ0pOMGFRYnBNU2RSMmZjN3NwTWRwTXAwNjk5K0Q0dEl3NHdPbkczc2NaVmxJNUtmeUZySG9TWDhwamgzY2ZEeWRtcnV0cDZtaDUvOVg2VGRZb3h1bkx4YjAyQnFzbzNHek1mZk56eVJHN2JuZnBJb09tU0NLRXE0OHQ5NFNkVHZWTU1ndEd5LzBYVmMrSDMzME51QW1zRVlFZi8xYmlnNWx6Wk9oeGVONXBvNUNWdUR4UFB2VTJwQ29TNkRLT0NPSFN1anRrOFg2TklCL1pweEJoZVJJejEzZDlPUE9Dd3pFV3Jmbm1aOVd2ME5ZVDNZUmovaU1Fd0VlRVNsVnk1WDRKTTFUaWxiQnVOQXExVEs3OGlMa2EvL3JVQWdIcXlPMysrQmJLVXlLRzBPY1ZTcVROd0RBMlh4T2RVeHkyK2dIOXhvMUgwbnZwS1VLOHYzWTlaUEFxdTdlNGV1TG51UE5pdmZtUXBMZE5DeTM3cjRpUUdkNXdjZVlUR1BKWWc2a05VSU9VL2RiZm8yK3p1WDVKampmWnN0WU5yTFBBS3Q0Ky9KQnU4N2p1L210SDdYNGRZY2R2MVF0eWZsWVJYUHNGelpkMUhnYmNta29nWFRpNTVCSGhuR081UDRBa3lLd1hFUDk0OVVPUzlmRlFEVUpLbURHOWlnUjJYOTVkejVQVFkrOFJPTGlTS0dUMktDdHVBTEs2c21iL1hmc3I4akw1clo2K2pDTXBrMmxoUlFSQVZiUWVoWG95a2RoT3V3T0xyZVFGY05GT0IrSHEyTkdrV0NkMlJySXdVMkJkandIWnltdk42MWpMekdSZ3RWZ1BrVnp6Q2dqSGRlL2FNL1h5R1U0MHhtcDNldExSRjlQWFBwR1JCWTNlNCtLUHZpc1NDUEtCbTFoZ1Z5Um9WN0FYMmJieURkcG9FN3MyNjNJbVVua3NYNmtHSTFWL3l4S3NEWDBUSzU1eXpaNFI3VXFjWmI2L01DaWlmVGdxeXQ1enh2Zyt1ZjdUZ1JPcU5CRTdBNnk3ZkJJL3FKTVNQMDhnc0hqM1NDZitNNnA2TUo5Slc3QURVV0RrVDhKQjVUZzcxTXFOay8rWWF4YXBKQjl4K3NZYUxUNDFjaTRMUi96T2dJbi9VVTg0K21zYnY0SjQvTGFLaU9jRDlONU5EcFdxcS81aFRNVjM3SS9YbHVramxmeWt0ZjRZaExtQUxiTXIyUVluU3djbnFaUXFuaThheDN5cWFQL0lIaXo5NkZGRlU1dGdjZlJvZ2IvUnRFb296b1BKVWVHd0g4ZVhMc0NnNnc4RDVjaDNTcUZNa1dCSzR1VW1aeFhKS0ZRLzRTSUlGRGs3RmhXTlh1Q3BqbW82ZStQbytDZ29nbGRkQXBEWnRMMzBjQyttWS9FOHBYc3dsaW1ad2MwNXJpVFJmMmplTUh0SmljamV2ekpteVVtK3VzOS9ERE5NaGtuZWtFWVNvN1pwZG5aWGNSVHFlSDRObVgrMnNYVE85bHMycm9veU4zWnhkWk9VKzEvUXJ6dCtiNXlFM2VKS2VOTkt2UkFDWjNRbFpIalR6MDl6amFZNlJPaXU2NVhSTjQ4Z3hSN1JkbUtVcmlkMUlGYUptWmU5RE5jbUkrWUdMdE1nanl1U1NsVjNCclJJSGdHbU0yOHZJYkFld1ZjalRxMUdWT3lCY0ZZR1ZsYmJWamlSNHFqemNjR0ordG1WYjVHd2c1UmdnaWZZSWQ5TkU4T3pjakdNcGZ2RkVockdka0VZdmZRZ21GWmhpZFhvVGowaGNLZlFFKzh6SGR1elIxbFQ4YlNvbHBoZVc1TnArZ0FTcms5UEhmWUZXVFlMNDVibG1pOXRvc0pYaklsV0VPamRPeGlPdmtveXpaajJ3N25oOWlPcWM5Z3I3KzhWWmRuTWFkbnA2OUFCc2cwVEdJN2QrK1JBTFcyUkZEOU94aVZmR3VWeTY1QUo3eDRqRzdjVVRDYTJKTi9SMGpxMGJ3cTgzRmpaeWFIRjhiNUZHZVNranJ4eFV0SUNNSEluZnVHMDdPMEo3bkdGall2MU9mNHlWbEVWb1hqYUFrWCtsYzBUa0xIQkYzNUJNRHhMU2JEOThPVGJwQ3BET28xaGdVazU2SmpRVXFjWDdZSE1rcUtkVnVMK0c3ZkhoYzVMcC9DTEtHaTVhZ09QZW4vem92Ukp0K1Q2elVPdGFtQ0hpSHhMUTczbVovS01VQXhvZExobWlNbVd5M2dLb0tuOU1EYWZ3UWllOGxsRys0dGZ2MUtMd3NEVnNHSUY5WmZwTk5oMXZwOVBia043T3Z2eWdobDY3K2xhSXZlcnowUy9vRm5OWHhJdE5QL0dvRWZocUJWeGJJZjZYcFFyU2YvWVFjWU5NOG9JT2FIRHZwZ1l6M1NONWtKbGVQVXlNQ2FGMTg2eCs3TmlnMEtrWStrcTQ4M0s4UmlCeWtGSVdTc0pYL3ZXL0cvTzg0bEpRVE5FSjlFM0ZoUHdNOC83dlBkNFI5eGtBQVhuRzZsUWIwOWluUGdzOUZCS2ZqZ3IyVkkwNk5YVlE2RjlXcWVEdlE4RmpiR0hJRGpxajhUTHJEdG1NUVBLdkdxSEpVbEJ6ak9teURXc0FXalB1bCtBUVluOTFXVklST2JLQ1pOZUg4TkNRVERhMVpxTnQvLzFaMnNrajl2Wmx1SnN0NUNNV1JwSkdCSzg3NVJhWDZFV09LRWtlckJiSWVWRzU3MVQwaWJLb1hGMW9YL2MzRlljZStHQ1F3Y2syY2ZzQXFIWUhHd3R1MGJuZ1h4YUt3Z3p1QlBra3E1QTQ2ME5PbVkwR3B0SGI1N01kTXZiVFJXeXhhdXBYM3U5cm9xMXdLVjdtTDJ5d3Q3NXgvZkZIYkFyRkxncGhBTFFEK2dlN3BMNmYyOTUxb1NJeFdnVzNwN2p3czdTSGJlNmdoVEdsQ2MyZjFONy8yT2t3a0ZEclFnWEV6MnJXKzdETUdOQTlWRWVsWXlhU3lNQ09HdlErbTJFb2ZjNDQxck9wcHBLaHdPdFZIYmVqVEMvOGpTajJhSmdubUIvMnYxRTNTZ3Uwc1N4L21jbzdDaStRTVdlRy95WnAvdTJjclo1UlEzK01WQnFWcXNHU1JPWjdMaC95dyszdzI1aWV1SndDMFlYbE9iV2RUWU4xbkZwWnFsZjNScGxpKzZMakN1NGxHeTdDSVhsdnZVbmIvbnRmNHpidVBEZ0xJZWhodVMxNGlnNDFuSUI0U3YwQTQ3TkJZekhITVBZRENkWHRLb056aXY1QmdDaWZYVTAzODJVcGNqMStUNjNVbmw3eUxEZE9Ibm4xc04zUzhxM0REZWszenNMcmpZUG1ZQmsvSEJuU1RybllmZ3EreVdkTjNFMENMeEFncDA3Nk1VOHU3eFBKY3BVYmtSMitiZGtGSDdEc05qVjB1bDF3TkZRUERteFN2Q2t4ZmhLUFk2SEZWd0hzTzJWa0RITnA5S3B2bUVFaUFrcE10a3lvQWlMRVpBeDhtY3lTWGkxRWZNM0NvWC9vZFl2eFZsM056ckZ4ME9XbWRuSU1XUDJBTTdGWmxaOGc0SlVBSExEazhISWpWZERXN2VPNGlhM3ZsQUFFWmc4L25WUHhMZERDZUQ0MzczQWhJcTNDbUw4MEtwMy9qQmkxQ01RVUliZVhJUHRqTE55SVZLZEp5V1oxKzVOaWwyOENWY3JtblFCWlhZTDg3ZVBYRHJpbmdjblNPbzVIZVV5c1NsNklBVWJmeFhiYWVEWlZnclhidWUraXIrT3ZlWGZXNThOM01GUkNJamVuVjRSUWxsRmxJbURTYnFFQUFBaW9yRDZUY1YvV2Q0cUFDZEFXQzQ3Y1o2N1FiWGdrSFFMQkQ2WXMwUWowT2lzNkRWbExFRU1SaFY2L2QydDNEY3VzRGN1ZExYZVhkZ0MyUldHalZEaHY5RlBXMHE2S2ZLV1ZyNHd3b1poTmVXTXhqWEtEQzlmSDNlNWdmUTVKUHVBaUdFOWovK2NzbzZyRWQ3clFpeGhKOUoxeTk4cG8vZnZhN2U5MFBBSXpsc2ZmcjNKUTVCL0tGOUhYZzQvU3V2dkRrSkVvMU1uSzNqclVJL040V0NmclBBVmlCaEZ0REw3Q2xoc3J1d1Q4a055aHF4c3FvU1ljcU5KZndyeE16eEo4Mkx3OE5Zb3IzbTlMMjAxT05BdE9ZTEl4OXltUGRwTlFTand4YnVoaE15RnljWXhJNnhLdlVQRzVDSzBkTUh4MlM4ZDZablk5SG9MQmFqTmtVTnA2Qlp1SG1mdVR3MG9OaGsxamlhbDRraEJiQ2lYbFZuT1BJd1NMYktCOUROUUE5VStmbnBURFdnSmFxckFWempCbHFyNHlTbVZKZHA5UStPak9NcmdGV01xU2VEdUlKcEhuclpCODdUQXBLSVlvWU50Mk1na3lodURYd2FVcEozdkk3VFNsV3lsclg1ZUNCR1oybGhrR3FnZ05qb0U0d3lYTGN4MU53UGx6cWhBS1RSb05ESk9mc2xCZDlwZFZzeXVHVjB6S1ZZYmdEZUxQUUNSTnFjU0p5T1JSYkZGdXc5dE5pcGEvUFZWcFEraVEvK004OVdnR05tbUlXZ1ZuZ1ZMcERwY3JLVFlBTDFnQzkrRHBpR1dndW5EWjBTa2x4dEt2UUZTMW1lS3BmM0dMeXFlZkpmWXBtdGkwOWgyYkNEdTdVWjFXd0tsQTU3dFV3UlNvaHhlRXkrMERGaXRPZzlHNkdnQnZmUFJCVm5Cc1loelFZS0NaYSs2dk5WYXJXc2JMNEkvVzdKaUNJTXJab2JlcWEyazVrRVhsSGNaWFc4eHZxaDNPNTlleWV0czB6M1ZJYlpyb2c3QW1ESXByeVFpUGg3NkxXbCt1RDcyaFJYL3N0bk4ybGNKeG1aUXJRemxaaHZOS2h3VVFTa0FiYXRDWUs0N3B3N0hLdlIxb2grN2dYQWRidVZ4UTY3UmxKTUZ2dExYRzZwMUJWSFV5eFhOcUt3cXVvbTQ1V2FObHBrbk9pSHBvOHowY2I5UWlHZllzK0hpQ2s5emgyTFJMSFNDekdXTkRJaEk3K2tlNWxZWGNVK3NXVlNSZDZvMCtsMngwSDBCV2hrTVBOWlJ1Mm1qbzBQZzIveTJ5bVpkNXNQOTRWWHVJc29Za3VDSlFDSW0vb29JRGZXamJiaVB0clREcllsRXJiL3ZzQmlRc0dEbjViMjhCYzJ0ZktTL0FKbVlRdG5JZm9mWDV2YkoxYWlXVkdyK2IwZ0VzbDRXSFBOeXZpZzdSYTFzT0sxd0lOTlNHUHdJUFk5YiswdXROUnB3ZVFhc2VUOExlbEJ3dUdIcjU0aTdLdlNIL0N3empMc1pnMjB0bitmRy90N1hHcTVKZnE5VzNDZFdjeEt1anJZbk9zZjlWc1UvVGJ4VWdpelQ0REZvVm5Ma2hrT2tEUzBQRm9zY1l2aXJpb0ZjL2p3YzdSS0VpeVVJbUhOMFR6VjVIcjZzekpiR3QzcGJ2d24ydFpsQkhjUmQ2elVQSnFwWlUxRHdXK25tZ1VFaHdtR0loam4xWW1OY3dHUmtsT2h6c0ZteldZbDJhL1RPVEFxMVdGVjBBQ2c4cUZFVzRtazNJdUcyUlJwU2dadzBCR3dHMENhWHRzc25LUGd5b3JGYTdhcU1UZzJlbWhMYit5U3lRVkpnYVNmeTdXdmZRL3l0Z0U4dHM2eVJ2S3VyeklieFYwRmQ1RUlIdHB3eFJQbmFCcG82eHRiUVcrYUZEa2piYlNyWVBxQS80TFBSUzZFK29nN0NLU0ladjRNbGV6dWszcDV1R3dVMHprU0hHeERIbVE5WmFZdlAzM3NOWTdwMnpjRW1QM2NMdFdzUXRsTVRGQ2RPck9GNFg0SkFDdVNYTmFOT1VJdHVZUnI3eDdER1N4MU13Uk5ac3FjS3NzYjJQTTA0TFc5QUlNVms1bkh6akcwOXFFcFgrbmxiTzNIOUNEUUZCanR6WjJobmtzSTA3S2U2bTRyK1VwUC9DU29RUkZJbzIyS1RxQ1Zyb2xraG9adXJvVm5ZSUNnQlJ3S1RPMk03aHI5L2hGZHZaKzREbmdLazJsdnVrbW42RUJFdkNBUlZvby9HKy9MQVhuenRldTBuVDhnbWtSdUJPN3lMUWJuMEdpOFRFeldEQ01vcHQ1Yk54emJlN0pGOC9hV1hXVkZtSjlteFl6Y2x0Y2pwOWhLNzFnUWU1d3FrNmovM1Z3M1BKdmhGN2pOcE01OVRQeWIwTGtBaWpQZ01mR1BnNGdGQ1g0V2c4Q3B1UkVQam9JaVVuNGt5NXgzakpLWkhiVnNRMnJaY2xmdTYzZi8zUUxZbU9QbWFkWXc3YSt3ck1QazZ1aHA4SDVsaStQSURuQW4yVnp5bkhTZDMrT2pPYXBuanduL1o0NDJKczFBbHNDU1BDdDRwYnJjeERDQlVnZWprMFFabGlmZTR1K3lqYXVzUUc2OVpyNHhvV01oZE80VyszZ01jMUxpUzkyc2NFVkYzMityNnBUY1RTN3NaZms0YmszaEpCc2ZCVmhNdXpUQzFKQm1BYUdqamthcGZiS0J4eGFKNWdzMmJhQ21ZRk01MStzZ3BaelVHVEhCOHloS2ZTRVRzMGVkOUtRVXFjcHlRZ1k5OE1YR3NnQi9PSU5tQkRGWGJuZEIwSW5vMVBYcW1CQlI0UndGOUQ2eW9RN1lvdzZvOXRodnZJWXFVd2RtSzBidzl5NFZNK25IckZqSXNPck5Wa1R4SHdMTzVoSUNPZzdXSkl5clYwOFlDYkVqZlBWNGtIQ2pBeUE2aFA5cThwbFloVTU2bG90VnpySkRSTTBkcHNtekVDWEp0a25ZVWdxdHNTSHdPRVBJMENQZzQxRnRPR1pGTHlPMC9PZHV4aGxOaXdLM1RTZzM4dDB6NkNUL3E0VUlCUnZYVFB2Z251ZmxDMzFQaHNBakVrcnhTWkJYTzJ6OEEwbGZEQ0VSTlRtTTEzMjh3NG1wNTk3N2tUc1gydWo3V3BlblFESmZ6Tk9wRkxEUXBoWVMxUGNpc21vSkUxQ3d1QzdzaGlBVmZHNlBDYm1aL0hCdE1FcFNxcS83b2d2clJ4akQ1T1Z2L2hUWE54YTRLbjhtazA5RVdnTFFuTzMxRllxM1R5eVd4R0ZvSldhenZTbUhWY3k1NG9NTEdEVUVCT2JZeE5pcStsMGlJZERUUXlOMUZxa0xhUDlBcGFzYXFCRkxzZ2h6MXJOY0lLSFpWMWNhZno5emt3WWtCZEo4SlJZR3V3Y0hCUUlhZUtoQ0x6NWJGSXc4UnJYbGgyM2FNamlrNVFORE8xT293UCtjc0lIQXRrdUljUXZNYXZjN1N1VUhNZzFGVkJOd2ppSlZ0MjA1ZnBDYXBMSkhpUkhHRmNoRW9sbEFDNzBPS1prTDc2Yk9EWDVCdWs0Skk4a3JNS0JXeThybXNibHloTm1iNVpnc3V6Rm5oRWt1SGFOS2NXcFpzakF0VUQwbC9ROTdZOXVUYXlIZnRpMVNLRUM2TjJTUDFnalRFaTg3S210Wms4UCt0SlMzRzFwRG1tWjlpWlkzNGJWenh5aDNnVUJxaUc3VHZ4TncyU05BMlZ2cVc3QXRTbno4alk0TVZZM1UrZ0s3elhoSmxXWElMUFB2N2MvYWM5RGgrNXF2NzN2alBFNnFuNWR4Q25uRkNSUlRCYnpYWXlKTVhDbGxXT0NQTVRwWUlhd3RNZ0tZQjF1REtxWFZ0M0JjK2p0dkNZbHgxTC9Ud05MbEk4eG55N2l2RjZ2S3ZJSXNBellFbnpRUXl2N0JoaU1kL2dSTmxwMUNBek5MK2xiaXdYUEZxN2pUZ0d2UHlURzl1a1ZvK2J2bjFNZ2tSR2lWOGsvbGVUMzllZkFmK3RHOS9wOVVIREplZ1J1aGg5cGlNQ2tvQzgxa1p3OWJ3NWx5OVUxM1dqaE12eEdUakNYVzdIY2VVOFg2Z09PWlVIY3pGeFpNNXNhc2k3UmdwTmJlakJEbHNxYUhhYzJyZW1nKzRPMVhyeGZ6RTZLTXR0RmRiL1FDRERpRjRsMWpqWE9QeGFxWCttVzJiY0F5SHk0eXZBQ0xTYklPNDRWcTQ3NlRSY0dvbklURnUrQ3dxY0pnVEs3ZkQ1MGp4Smt3WkVzWGZsSEd6SzlwWFhiTW4zTUx0YnBWeElDdnhPbEJOaDBqMmlKNkNjVG5GTFFURjZERXNUTm1WS0ZRdGsxTlJrMnByaGpkQy9tUDE4eldsaGpPeENjMXM4TzVTMlV1YVNERkJWL1ZoVlIzSjRua1FpVUdaOWE0TndkcldlOVJKeDZDQjV0bDFEaHZxMmd1VFNGYlFlR2trTGNsMmdrTDFLVlhNUGo2V0E0cXM1cW1nS2pWV0doaTRadHliOVl1Q0VxKzNpSnRUcFZKVU9odHlZRWZDTExJSXVzWGJhY2hubGozSnIyVVlzMnJzcnJpNmR4aDhWQ3hGbTJPUGZiMktLZkN6TXE5SlV5VitHSExrRUNOSHNVYTJnZXVDTXRIMkZSWTNsRk1UWXU2VVE5dzVEU3RSUXM1MTdxelZMUFdRWk8rKzQ3SFlTNyswV0JhU3RXTXNENWxZUHNMb3ZCczNMblJ5d1lYdG5pbnA1eGJqVitIdDg0YkovV2ljTHpPcDB6STU2QWt6Z3E0SEF6WW55alI0SkNTdWxEQXdwT0RDV0xBNnNkQUx4dnVib3lVQStaTVJUU053U0VzM3NHTXFKeUlYb1h3RjZJWmtpeUUxZnRTclNxeFQ1SHJTVjZOdG1DczZDM3phSmxGRnVVRHdEQnEvdTAvSk0vTm03bENObGVSdXZNQnA3d09NaHh0ZFU4NDJvWWZPbEM1ekRIZUFsNHM2bWJBNnU5cFFxNWtvMG9MeU5NWmNiVCtrUVJJMWwyWERXSWR1RDhyZ1BYbVdZZFQ0NmZzQU1nc0VhSW5jdlF1TWo0MVhtV1dVdzExWmdITnRXMk1vRWdaaURkZ3VFYjVJblBkb3pHajl6MHR6WE50MjJVSjlmVVlmZWZnT2IxMFNDeHMwUjhFS0o4bFN1NXN2NGV6eEJZVGhwdzk5NXhSQ2wwdVlmcTIvVG10YnNkMEtaOGt4VkJNLzJoMXl1MzhnNUV6eENtaEdVRnFPM0llNEY0TzVaLzY2aTBNc2hyL3Y1UnpLRmJYVEpBbUJ3d0c1OXhsWHlxRkE4V0V6NWNoSHhhaExvU3RxY2d5VEE1TFQ1WXVHZkJGMnRUdEphVXVMUm5jQkNESzJPdHVnNGE5R2tBSUVYNnJaZWYvL0NUNVpKRVIyWDVsYVVtNTgxK3FENHp3cmpDTnFPc00yWEwrSE5XMVllbm5UZzJidmgzSkpMbGIycUdrQ0w0TjViUU9XUkdNQjI4SUF1YmdsYldNOWdROUE4WmxRMXBlOG9uQ1pqejB4ektzYXhIQWV3Q21Mc3ZPdzc3eGFublpka3IxcDJPRTk3YmI5ZFVNQldPdzdWL3NFWjNJZ3ZGbDVkWWduRzBzd0NpYjREb296WEZiUUpMdWxzcGJncmkzYng3TXBFTlFLVkxEcktleGs2K0NuZ0YvUlVibTRZZFNlblY3cDRUTmlkQmd3R3owem02blN1bUQzenlnbVZqZmRLNldLaEtseUl6SU5iUGZ6TzdzdWRMaGtYWkNDSzNScHVDUUFRWGxianl4cm5sWC9vbnMybVhnT0dBQXd5ajJaQVdMSUJabDI1SEY0Mk8xYmJaWDhzZmFjWW8zUjlCSGhtZlpNN0V0QXRVREY5eEtzZTQ5UU5jeVo5WlRQbEc2VVNsaXAxUmZFZTJRbWhiQUpoRHlBNzdNU2tiREV0OXNMMHlabHU2Sm5QNjJyS2xMOURhNnZlaFVzRStXdU0wRnNxZkJCUzlpcnNKSW9jRFRRZ1FXRVJMZWpSbHprMDNEdVdtdHk5Z1hFVm1xN2hPcXZDVjJTSkRMMVdSRWJVaG5kNktrbjM0WGNub1JXUmxNbHlxVUIvOHMyNVU4TkQvUUFTUXFnYTAxWHdZL1BEOTZReGdMaERQc0lQaVI1UFNCZFhMdzcwdy9kYWxNSWdqazFOaG10L1JlSUVMa052Qk1qRkY5VXh4TWl4T1BwRlZrK2ZXU3VDVWdXRmRvNXIxMlZWQkZtdE45VkVwVDZxSkZOblZudGE5eUMrMndjUnpZVTFQZ3JrZEkvM2R5UGwxR0lTMDZEZUUxZUJGZWV2U1AwMXNiV3ZIQVhWSEJKdE5kUk1zT29yN0dFZnAxbkZXZW51UlFoclVZRTFkZEYrL2tUd2lzMEZKMzhrYWhxSkdwWDZubHpYNGI1UEQrd0pOVDZKM0lDdnpSTU40bEJya2dJbElIdnNHdU5xeWpLblpZWW8yMEZpRms3dVVidkVFTlBPRG02ZTZySFR4N1dTbllkY1RGQ296UW92RThubnNSQ090T1NybUlKeDNBYUZmdU5ORUdJaTlnVndYZTBGeEY4SCswUXVNYzhyVVZwbFFYR3ROeXY3VUFqbmpkWVFTZjJLemwzajgwVEpXUFQrNDR0K3FFakw5ZC8zby9QN1VDdHQ1VnhTWURqUGRuQ3J2c3dYd3g1b1c0SWVSVmx6RmM2U3JMRlNxZWN0UnZBKytEbE40bTlsbVFHbXByV0FuN25RWjFlWWE5djJzV1ZncVEzOXgydDJiOW9SejZqb2tKTmFkRzZ6TjIvY1hqM3VieVJULzltRHd5QVA2NG1iNUNZNytydytvbkRFNkhJUnNaZDRCTFZGcGZEL0hGM3VwUEk5ZVRnQklta09NNWc0bTEyMUFXOHdwZ1ZWcDlMdDFTeG1ZUzZseklVSUh6Vy82cE1HR2NTeGt1RUhzNC8rTG5LU05WeVFyMzdTa1RDVnUrdWsraE45cTZpRk9KVjhidjIvMWl2RmV3SGp3ZkRBeFh6L09KaXg0aHlYc25RUjBPM01QZlhTeVZ4SFI2bXRJVHF6SDBIZGRlai9mZFcvbU14VVgyZkRpazFaSjd4MzFjeEphWVBtOU1hMXlBcWJESDFwUFoyR2V2UTNhelJIZVBLUGRPaEh4UzVxWFJwTVRxd2xMV21uL0U1dktrWTY1R1N6emgvY2dBRGpMcjRkdGFSZy9jR3dhOFBRcTRiQlZxbzdiMXlva25qUEgxaW5nOERTK2hSN3BYZWFPQ05vZUZjWTdKUFh6VzY1L0t3c3A5NThWZ1NyNmR4dHM0U05RNVlLZ3JEYkttVWJTSkZxTkV2dldPbTFkWmxnUnZibjM3d2dUbDlqYmdwRXh2eUN0Mlp6UzlNL3BjRVcrNmZsdkY3TFVydWU4b05UckVRbCtXYjExTlBJRjIxVDI0SHUwL3RVWjl3Q2RDTVJpTkRVNWJUZnBKbS9PdUhWcUtSRk13ZjYyRXZBWGRkVGJwR0tOK2syRC9XK08yb3JJRm9JYjkwMHFyRlpZS3lmdUo1MjhsWnM1OTBDSEMrWjFybTBPMUpIT3JLSVZHNDNLRXZkWndNdG5qckFvRkZDdGt0aTljcmppOXd4REpjSmNTcitkMyszUXVqcGpSK0tMYm9tWDFnWEh5Q3Zmc0ZLUWFyYU42b3JiU0FLOUxzV0FjVHBUaHppb3orR2dyMVVsdllWenVhdFo3TXYrZUxmNHl1YlpCRjBCRmpMemFHMjZPNzFJWm5MZXFuaytLSkMvWFNFUmdndmFGNHRZZHZ5TVMzYXVGU2RWR1BFTjJLeWxBd3ZMWitlRUlJUktUQ1gzeGNtWFNCS29hakQ5NllBRTB6eCtTSTNOTFFUdzV2dFZXSFNLMm85ZjBYamV4Mlk2d0tYUDhGWVRJMlRjMklVZklWend6WVgrSk4zbStCYmxQdGVYUm9KUCtoRUNpZVpLcXV5dUlERjVaWkl0NVRJSXFIWUdmMTQ1QkVYZ0dZTTRURDB4MjlQSFJ4ZEZoY2V1NDdwTU1pcW5JVGU0czdnZGQ1WjJNcEFVVDluTkRkNUw5dHBEWUpYanAwa2FZTDhFNjUzQWd1Ym9RSXpzTC9FcGh0RW1aU2tLOEt2WXk0S0t1WXlvUGU4K0tKM2tCSHJlaDZqT29GcXlYRHB3L25EZC80YVFrTFh3NUpYc09ITzBVeDVOYVhXSWxGRVk3YlhIeXVJb1lNMDUyRDBIUkw4NEtIRDVuRkVKM2dCTkw4SUhEaVFRZkViQlI2MVlrTWNJN1lVRUhkVVlsMXJwMXhlZ3RwWndZbVpDVzVxdXNPL1Fma1pVM1QrUUY3NlVYbWR1RjYyaS9KaG9UK2tocGdPeTdqd3p3ME5QeHNpeHVrSUVZVDFNNlprcExMTm84dm9GcFB2bUtNZWtFczBYNjhVOFpmR2I4elBHeTVBVnUzeWlUS3pkMU1yMkVQeFVmVTJvWjZiL2pvTzU0ZVZWdTRsQ3U5bVpRQk03UlZ4ZjFXRkd5SDNjSkxHNU51K2t5VVlNWU0xcDh2VFhjR3IwU1lQZis4KzFFa1lGU3RXTjI5a0JhREZVTmp2U2VNQWVpWDZ0RmZpenZObVFuekRxbkRneGt3WkhkcTROU1NDYitQY3A3MXdSbHhJMXg4dldoMC8yWExDTEZaQytzbzNRUXBoTG1HZFpOdnFZeHEyTXJSRUJyKzlDRjIyQUlNRW1PSmpXRkNFNG82anZHUFR6UzJyUFBGOUZOYTU4UkdOdm1oOVQ2bDhsbk5JUHArb25PQXBuZHErQlMwMUJuRjJXRTJKMzZ3S1A5U1BxdnljY1FWOVkvZmFNSytBYzBOMHVuVk9QZ0l5R0R1Y2xweHpqYUJGQVdFeWlBOUh5REUwa1pLRGN4UjJCMzJhcFFVaWZveUszQnhMbFNDaGxCVVNXTjU3NVpIRVJSK1JnWndyMDdKdllrRjBMbE56d0NKZEtJZnpGRlZMajVvRk1LTXNpcG5ZLzFlZUJsTHhoR09ycVRsMDBLeGNISlE1Z0N6dVJjeVgvR0p4UHJvR2VPV0szbVV1NTdqS0R1Z3pjSzYrRGQyc0xnSEp3U1U3ZzhrWFNaa0lNRk9tbThaaXZzdDVxa28vYXRoVGRhQXpjRDgvL2FyRTB0anZXb2RNZjF3SUF5OXBEeFZNRTJ2QkIwL1ZCOFpzNmVtLy92NmdzOXBqSm9QZ0NoSTNBRDd6SGxzUlN6T0JReXB6YUk5cERNUk5qYk1GcVJLN3N2RlU2b3kwdmxlQXF2TjV2MWY4aFFDb3AxeE1FL1VzYk1udmt5U005VzlkRjk3a2grY0lNNkdhK0tlMHlnSVpPeWJlckdpZjErZi82aGNFdmY2dzNEaU5BcHRIQ0NYL0d0K3pPcXFmVUlrSjRlN1NkNU5RV01QbFQvWDlSL3ZjdThIWC9VZVRlMTBIVUtpTFo1eVRnWVBrT0N6RktZblArQW9FTzJQVFZMTXRpQjN5K25FVnZod2RIS0UrMnYrWHpMdkNacm5jK0tYT0lZUndva2o2VjVHNUUvTWNwOCtBNFdqbVJRV2U4U2ZqbjZ2SW1iSm1CcVJCeGFYN2ovYzNQRmloL21yZWd1SUZvOU13NEhvSEg4NGZtOHJ5MU8zeFBIVHFPc0p2S1BBbHVmSnNkZUsrVEpUK2U3NXVyNWtUS3c2ckFSWHYrSGY1elpscTBudTF3a2ZWK1JRMkd5aEV3QmtubDhoSGtKN2ExK2tpbkZ3MVRGRkg1VDFMdmNxZkl4c0VnZ2JYVHZDVEE5cHV0N0ZhdW85aXVSSkd2aHJvd3FhcDJsbDYwbTVYbHdoM2V1VFc1M0FNbGszTmV2UExFdlVJNUJDTGJXSlc1ak5HcXR3ZUVRWkdiME5HSTQ1d1BuVGE1dnFzbjdlZmpNdmphN2RWL24yMWVJRU0zN2JlUFpDOXBMRHdvTnp0SFJyaXNuKzkvVTliOGt3M2t3ekNHU1Z3eU9SWURiUXJoQjRRMGd4bDAwQjRva2JtRlI0K3ZrZi9DK0p6dXEwK3NVZ1pCNk1heXRJSFlRa1A3Y3czK3dVeEVWcEUrMEZqQnNyTUdmOElpL28rT28yNWhPNmJodlN2ZmNTV2lPQmlwby9yTndjMlFsTGJmaFJGUEtNanFDR3JGN1k5OGlpc1IvbWJoa3VmNkU1ZjVFQjBKaGNBcHl2RFA4VTh3QjVSZzdwTk5FWXU3TUdOdTIxZ0pDcElrbEFoQjJhazNXcEFUa09ycUVpUU9yZXdkaExQRzhhUFhwVVI4S3M0Zjh6Tm1WZHBrYWxlTTlvbjI4MlZON3kwYVYyV0xua01lWlk5VWtHbHJseEI5di9uMmxxSktMLzFycHNEVEZPWEVhVmRlZjJZQ1Z4VjZUOEpGSUNqT3pkaGE5VEpvR01jMVd0S3ArejVHQVp5ZzZKWmg3ckV6eFFiYTcvOHVYWjhmN0xxUGwyVVNOZytWOUZMTFlCWjE4aDl3a0NjcFJNQjg2SC9PTmhSZHIzc3l5NGo3eW5SSGdMYVBzeWQrbm1qekpEV1A1U0hTYmdJNTY0ZXRxSUhYeWhSNmRXU3dqVURVWEJLQ2JDYm0yajlmOXBZR1RXd0RYVnVnbWhFbkJOUnpJTWZneFpHRTQwTXdQejZ5bFFuSFB4NUhVUm1EQjVrSTdkU1JVelpLVjRpR0FpQS83anRLVllCZzVtQVA0N0F2cmF5LzJTWkFCVGh4bWR0c1ZvYW4wbldvb08vU1JnSW84T1VtdUdsOHUvUmIxT1FaUDJWaUp2OVpxWE53OGRTVkhuTzEvdE5jK2wraXdOaGRpQXRydTJFWTRlMXRoUW4yYWs2Y0pkQVFLK1lPOHFzWWFaYXNBL3NkY21PSExROXhBSXRnVW1KUzFHMlE4SHVBc2dMaXpkQ0V1dU43dkZIRmNtTHZUZDZRTmtJUGdUMCtDNlFDd3NBL1I5QnYwRkhqMTdLc2dIS1hWZjFXZDBVcDA1a0Y0UjFZTTMzN3dUd3ZTTkZIN0RLeFdXeFNkS1VaMjNOTWZkM3cxNW5HMzA0dzhmMGRoQ3RvOFlpcjNTeXJKSkU0SzZ3a2Z3REFYN2lWK0RTcW55ZHVVWW1qZmt0UUZ3ZGJZZ1BmdW1aN1V5cE1jOG9GVlBCSGJMUTg1MTUxYUF5MjVTRGpxdlBLaGVmUXNWQ0ord2ExbDY4dVM5VDRUbzZiRGNrY0UyVjEveXJpbGlqMlc4RDBqUDFpd1pJU2NKM2VkT3NMSDdydlpoakRsQXFuOTZtZ3RJYzF3aGtpMEtnZmRhNDg0MmZZajRzenFLbmhyUllPaVZEV2xTWU9haTBiMjNuYnorSlR1Z3pia2F1VDRFb05YR0tjL3RYR2ZFYlJlQ3lFb1dwMTgwNEZnSDBrYWpoM3VQc2wwYVlrS3NBU2VOSHN0Wlpid3NWZHBFR0RIQlEvTGJMTHpWMGVYbDhPMlVWZk1CVDAvYWNqbUdjR25OK01KbE5vS094ZHE4UGQxMENSWHR3TmcwMHJEUUdTSm1RMS95Y3ZvZ2JtQ1orVUp4dWFPVVEzRzV6K3JTYjUwQTlqaS9MejJFZnJhODNyNzJJc0w1MnRxU3ZMRFJhaU1ReGMzNUZYem9ETWlkT3JBNXJMbVozQ3I4TEY3VXZoUVowTFdiMUdQUEJQdzFXZFZ2K2Q2aG5ldGZpYUg4VHRpZys2TStmVjFRNzd2UTVTMG0wVHEvV21hVE9LMjgrSnJRdnFzN01QaXk5cEVDdm96ZmYyaFB3Qy93cXh0dWxHZFY1MXY2NmR0ZTQ1d1dtRTYveUwzb2wzUFExOWNNNkJ6TDRBNG1NVmI3MGcveXBtWWlNeGhZWWh0VjdvZTNSVFZZSjhKcnFYbytPRXBNanB5Qk5TWHd6VXhmUkxnSWdsaDByUTJYMjg5MzErQ1FSd3l6Sll4dUdwWm9ESmRabDBzd1RBKzdOdjR5Y0xiajJBMnVnK3ZxQUs5NGtrajZGVWlPd1pwZDBEWE9Bb0tKSzB0NVNScGQ4cXBpOGd5R3VnR0NCMForcmhDYW5NbXFBTGptSGxiYzFMV25pSWZDNk1IcjZXNFZJNTZNcmQveTZKVysrQWJKUTRxdDFDWk5SY3oxV2JFRkY2OTcrUExCT2QzQkxvOC9sSVdHL29icFZ0ZERXM2duWHhuNkQ1ME8vSE5aUndCa29DVEtheTV0R3dXUFA0KzJLdkpwY2QxZVI0cWdPZzFxNWJBUHVvT2ErVGlOVzBtWFF0bk1YNEdGR0FqY2VHVWNka3ZPY1B6bUEyVUtpRCt2T0JuMVRqdDkrVDZJME5GREJyemRLK3g0V3VpR1Y1dFNmems4ZUxnc3RKUUYwdjlvb0VtZVRXZWwrOXc1dHdybHp3UExocXN4MWdQZWRXZjBieGI5eUJNclhLMFR3bFJnWThOSWtDek5qbGU2eUxXRndoQU9WZkd3dXQrblVCem5qa0oxVG9kclZDY014VjAyQ2p0ZkUrUFpENW40VnZVN0RTRm9PUXA0c2RQcWZPOCtVZ0R0OHZ3T3NFakVLRER4MHV5Z0tZTDlwR1BQTVZHdTN0YW9yVHhuMk9SWmd1d1k3UFo1OVZPRHNOYzM3aCtId2lTOStnZ0lIV0tCdmRSN3d4Mi9pZ1NhY0J1RVZkZnBDWW5JUXVhcTdobVBib3BjRFRCbEdKWW1TaVZzVzRZR0MvelB5VXEyaXhZamlVdUZmMmE1bklCTktZN0dZeG11bWI3NFRvTHpwa1NVOHEwSUpSbldBajdEOXFHRlE2c3pCQXEzRmJRYUVPVGdiS1gzSFI1ZmE3SmwzTEEzVHFxMmFlV291L0RjRmFvS0RSbkdld001V2FiZmg1bmRLbXNlZWpMaThmL3I2QlZvcGF3a3I0R0RvcFQwenUzdDdUL2w1NFNQcWpteTRhSFIzWVBQQ082b1Fna0Zudnp3TVQ2TGtaMDZXQkpUdlBnZVB2cmd0NjYvNTFDak01eWM0dzFuYkZScFh0bjFhYjRwbDlrSkVwMmZhRGkrdFRkWGllUkZmMTF6N2xwNG42NEdRaWlGdmRjcmtZa2M3cTVQemlLY082RHlQaUVJS0dtK3o5WThuaVFWQTZWUk16SlFrSU16NW0wcTNrNnJldU10YllzYU5nWmJxNS90bVdBeWpXNjhsOSt2ODg2dXZlMDd6ZHIrZ3JHS1ErZEowT3ArQjlHeDg4Q21nelphcWVIS2ZUTldsSmFFM0I1bEFNM09pNWhyTHBYdS9HSlV3TFMvblRqYmdmeFhRTVlLNXZLY2FtNmVTaUo4SEpvbGd6YU5hMGV6bkpDclk0Y3RsZnNhckIxVzBzK1ZnSVRIUjhCc3NQQ0RveWRWRUtOT00wNitiMVp3U1lwTnNQczREbTR2VjluWDdTNnBybGxpYUZqdWRnbURyVzRvb1I0bnQ3cVFJT29YN3BESWY0SEsvWWR6c3RSWktMTjRmTExCVkZqdDJXbFZkTzI3b2FVd3dLWDhRQVJUYUJnM01USjhvQ0FELzYwWnVTQVcycjh2TWh0bzJzbVBuVmpQaU1Sb0tpSTFHZm9WczluT0NBcTRFejVWWmg3M2J4aUNodW9WQS9KTlNYRGVnZStCejErd0phYm5GVlVETlM0SzI0TGNUaFZYQkU2VE5FL1h4K0dhZzF6N0svY3BHTlR2Wlo4ekorRTVlaXByazIvOTRwSFNCWkltZ0N0WHRhOVdUV1RRMElGck04WGJSYTRrZ1NMcktuN3JkYjEzNDYrQzdXcmFrRkhhZThGUHliRHFISEZpem9RL2lGeXJuRWkyMVQvOWZUUGpBdzlqTDlPZmZ5Tllzc3BwdWZ1V2c5Zkx5R1dmYmY3anp3VEdmYWFwcFVvNUhBS1k0a3IyeWRHYytxWDFQekg2OGRqbk9UcUdYeTVsaUc3UU5SMUswdTdlSGRvWUlaSitGbTI3Y2ZlbFNPVGhPaUpVSkozNDZIc1BzdkUvWW9CZnkzVHA1cGVOWVFjNC9BcXpob1B3Sk9MK0I2OWFIbkRFM0FCZFRLdXJkSHBITndSZGMwMlR1dXppN2FTMXpjcjNyQmx2Y1I3dm93bzRmUnkvNFc1RlhONW5TeVdUTGRKbnBPMnIvN1lESU1VdDZTNVdEOTl5Y0d3M2wxNzNid3RDWGpTbTRWWmZCZGhTQTJKMTd4U3JGeHRQV05EdWdKYkw2NlRrYUtZbkI0UFhnSCt6VGtOL0J1bmY0K1gwNXAwZzlUT0x0ZzkxSnkyOUl1U2NNTFFhc3BwaXpnSzA4K1lQT0FEbzhNQ0twa0R4UUxwbitUb285N1ZkM0hYeWF0RjZrbXBnSXpOV09JNFJHQUNWQXh4WTh6VEF2T0xTZ1FmMi9vSmVTVEg3TUhiVGZWUjg1UmZIMVBiR0hua0VPZjVlM0R5aWcxK1dEQ25ZTSs1SnFDKzJLUTU2bFJKNDRzdmhrZlh0SGZTdnJIU2dvT05xcW5FdXF1RmVoZFdKbFFyZmh5VVlDaTlNejhSZHhSbWJ6VTMwbUx6M08wUHBHcjFRVnlUeCs3cW82Q3FPbFNuWkpUb204OHF3S05CcXJJR0ZpNGFkeHZFNDRSR3owS3Z4TVFZZXc1aWU4aTdNQVpMazBEUFNRYnZUTi8yQ1hYT0dIOHNzZ0JBbHdtcjA2Ly9SZDduMng3VzFpbVFzR0pCd3RmcE84K2xpK3IvU0k3OG1tUGJJNmx2aHZhRnFRU2c2cXloOE9TSlNUN2hSV3dVL0F3WjNON05ZdG9vUUptM1lpb2x1cGxvc0RUZTR4azk4Vm1BOGZRM1VtMm41ejZJeVZxRnE1TWNCM0lnMmF6ZjVURE9wdnliYmZSUEtSVHJ5VmQzYWJWaTEwekNYRnVOcjJISjExazU0VFdDMDF0WkVwZFZuNlFtenN6MW0wU0YybW5ncDF1eWlIOUZnTGgyeXlwajloblhrWXA4cmRkU3RnSHBPYXlFTjRBTm9QZUNYWmxYWlpzZWtOcFduMGl5NFVVTlYyUEhpUXVHdmpkNGNGWm5KWTlvL2x5MUo0bHp4NHE2MkpqZTdPL3NVNWFyYjdIRDdMaldWRm9yVUtMb3R1WkczbVhyNlBOYnd2bWhzTXdvOEc0SXFVM2wxc1Z1NlZtMU04VThNSXpFY1NZMjhjZTJ2QkhJZmtUeUFWVHJMTjRQbjk5bjAydk5HSkFnSFNQNGpQWjk5VHpaYzZxM0ZWalFUMldQdktmVEZJVnpCRjI3ekJHWFVUV0VlVUlJUE9HUU0rdWFQa0lOUFYxdUxMUlcyUzRZNk8xYXdCVlRWVmQwZ3Y4UmpPbmVSTEdwUDEzbHZOaEkzUlFLOEVEdmFZcGdYWklJcC9PL1JyMUxyeEVIN3dETXBwZ0MxbzBhOWkwK2NxbkFISFNCVlhmWEdQTU9aOHlEM2tHQk02NkhWa1lXZkdwalFMa093Y0Z6ZXlSaTZQL2FLbWt2L3hIUjJYQTkxNlRHTHMrZzk4R3RCeFRyVGJ3UndkS0ZJUXFPdUhiWGFRZjVacDJpcFo5dTh1TXpsaWhxRFRFeTlHaHJxZjliWDZhdzJFMmFKdExsV3A4N2ZTaXlGUEgrcWFsRHpNa0hTWFk5bTRlNElRSW5VR2o2K3NzSDF5Q3lMQ01XeGFGcXZjRTFSalhHZlp3cko3dm90OEhzcGt5WCtOVzVsQ2Zpb1B6M2ErVTE4Q1ZsdHBaVGN6a3hnYzUreCtGRzd3RnZIVi9ra2RYMVVac3JEc05GTE00QTNhM2MzNFFVbGNjdlQvajIvcjRUWFdBNWtCdU1jejRLdkkyUGF1YlRTRy9zUzVwSmhDSm04MHZuUTBLREhXdTFneXJHT3oxcnhVcEFJVmlJWm43WjFYeU1hbWtQMC9RZWdZSlhYcVUvVjVGRGtHajBVMkh5Rm5zY1ZKWWdyNmliZlJ1WTJRblZvZFpvMUxMNFdKTDhnVElkbEt0eFphYndZZE9NUWg5dlQ3NUlUbDN6QmRqbHE4WWY1V2pzQllkMUIzVVp5WWorVHVFSy9Jck1ERnJ3QUNpZUV6UFBPbmZDUEp6VnFrdEd1dTBIUkcxSXZheXRFVDhURkdzeGlrWlZPVFlHTDBRalJ0aW5yNmZ5VlVWeUdMZ24yOGJHNGhGbEljUytBZzVHNjFScnBVL0NXalVHaitFaStnTFpXQXY5RXdscGtkK3YxcnF3d0VkVStPTGpiZnZMeUFoWW9IU3NLR0h1NThKRVUrOVJIZUg3a3Z3T0J4eXFOUnQ4SmM1cjkrWkpSWlEzRXZYck9Fd2FET1dySUZEV1I0a3doa25YVUNPVTNSQXJUbGdta1cycnN1R2JvRHZ0SC9TUGdDZDBpY1RiOFNOaWxreXp4c0kzTVp6V2U2VExzN01Nd1lKK3IrM2ZWYkJUTWlaQ0c4OFFpUUtmQzdwVUs0Zm9iUmtpQTBIcy9DVDdPZ3E2OWZWSkxJd3ZSK2I3RmZvVkJBMzgxZ0N3R3N6TjlGaXBOajlKaUxTQiszQjJUTXlVaFN3SzhhS1p6N0sxSUZyb245SWRzZlB1eVVGM21wMmZuRnRpR1d0eUJiaWpoNWxjbEFKbHc0clpWNmZqTkUySjNxby9YMGUyeHhLK3dvckJxMTltR2hIREtRZWV1V0lyY0I5WkFUWUwzaWdROC9GVXdqOVloU3lMbXlxVWNJckF1QmhVbzIzNEdaa1gwZ1diZittUHk5S3QrN0RqTlZWWFFOVmxZMXhhUkVZb0J0b2Uwdyt1RkU1TEFjVG96eDVlYVNuMmhjT3Yyd05zWmc4VHE1N0t5TFFGZVlXSEJCbGNJS1ZxL2RLSHhObE0zMjBlK1pqTUVHdGd5S0pVZEV1QXlnanpEcndIalhMNlF2cDZYbnRJNGtEOGF3WDNsRkRpcnZZVFlHd2poSnRCZDA3blpIMUpPcTZobzZhcnNaTWtBaHk1blNEZXlwR0E0WkhrM3ZKaWkzRk55Vlh1NDlONWQ2Y1VrTHdkSmNDMFRIeEEvSjZzdVdHUnJTNDFoR2dZdVBrUVNnRktzVkFydlBXNTN4eFpDQVMwRWYwc1QxeVh0eXUwTE03eGZjV3U0eWhrL2xaMkFJREFyTGpQRE10WlhpUGx3VGF1OW1EVDhpd2RHeVRJZXNIUGZDVGYwdGVtT2tESEFRdEhzWWI1TXV6dE1GcnQ1aHQxL0x2SWxrOHpScFZ1NVdmY1lnVDh2L2pEL3IrM0ZKWkU1V2tlTCtWL3NuWGVJODNMZnZZYVVnUzNFbERvVjZDTytteG5tV1g1eDdvd3V4M2hXeTNYSi8rQi91YUIyKzJQanBkdERianVOU3pGQW9uQS81MnBLWkNaSDN0aEE0QVZ4OXpZRlNTZ3R4L1lpUTdmMjVOMkEzaEJYeVFyb25VUEZ4VERCN0pQWjd4QmtnMmRXRWVHZzUxTnpEZFlnMk5TREVyVHpyeWNuekdUZVRtRi9DSTdTVjZja3FPN0RFalVCWm0wUTVRY0g4YmRVS2xmY1ZPWGVPV01mRy94dkJ1cVdNVkR4b2lsaU1sYlRrSlFZR3lGUThrUmZPZlRFek9NQ2ptRDB3alBGWXdIckFzVStLUE85cTRsWi8weVhoU1dPcUlPNStxS2RCMUNRRnlIaUhrTTRlYUU4U0pIbDlJMDFSQk1MemZ4ZzB0WDlvYUtrZk80Y0RkVlVrVXgrSE1DQzd5Q1QveEJQd1hTRUVOejkzWitMT3R5b0t0YVozNEpzdlk3MnBQSDVKZDRydENobkl3aUtKVWlDNUNVd01nb2dyNFVMZnRadUtmeHdZalcwTXFPQXBLZWhvankyNEYrUHB3TEkxQmFiZStKSE56cVBhUmJHTFkxMnlpZnMzcklYWXI2WWdGSWNXYXhoVEMxblRjUmd0eXBTczdodkdEcCtKLzErWHRDNm5XeUF4RnRXRlBqMUNZdVBWcDdIUDFrbGdBMjBlQXlFTUZJRnM3V3FEdVhTNFlORHozNUtCQmozbHppVGtsTG1Eb1NJaWRpclVYNEJHeFJoeWxRSjFDR3NIS2syZ2Q4MmN2cjg3Uk9xby9mYncybUNCcjk2ZVprWjlLYTZZRjdoYklUVjhERXM2M0p1aXdUNWErRStlYWhUTSszdnYxN0VNY1c2bkFXaG8zT1dIdndVYmNwaDJ0eUNTRVdvNjF6RjF6dHE0VWNyWE5aR1BGbnZHQ3BEeDBqTFFoN05OU2N3d01wbTRTcHVuZ2QyYTYzWnFwUjVoZ2FkeS9pOE5XSmViTjU2b1BiYVBIYmZmTHZNNHltaFZiZGIvS0JGSGZqYnFPNTBnUHQxckxtSXZVV2xKVng3RzNpbEQ5UWJJMmIwOFNrbGs4U2JNdm1BTUxOU1dJTUgrVllTQ2Q4U0tvODRvVzFxNTJVdTcrZUc0VWd2bGV1OVJ2bU5iUmc4blpHOWVCOGFjK3ZYMWY0czU2a0ZUV25tNG9jSENMQVVnL1Vlc1lPZzFJdVNHU09TYzM5bXhGMVVia2ZUeU5RaEEvTjEvWmlYNXMyMXF2dGJtd3lxM2dNYWhsNldabXJWMjFuL2lwcm4yKzJERVVhUExtR0RtQXZMSWY2V200cGozNXdpOFl5YndqZmRVVHFuQzdOQlhCS3ltV0oreDh2R2dZTElxS2lGQUdqUWpBYU9WWWNtd2VnQytlRHl6T04rd3RQY0JZR0VZdmZoKzByd2pQTGYvS09walFJZEV0S0dqd3NuaklwK2xkRGxXTUJRenNTNVJNTm9LR0VPVlo1K0VURkE1SW5ZVXBqdmlQVkFWaFJzZ1JTMW5GZlJGN1lzcVBFcGdRVlN3b3hkcmswMDBmZjZ2YVZvQXRUR1BMMFVwRlpVN0VrR1ZsdGlQdEwyL0xvSVVkUG93NDU5c0NTeHU0U0FpTkt5S3FoSDdwdjZQR2NXQ25FV3hkQnpDbzh5c3EyVjdlaUdHMzJ5aFFQdWRyOWdSNStQRlVDbnZNcnpTNElGMEVkejV3RUtKb2RJUnFXQk54UzExR1Ivak9jRTB4blVrSmk0eXQ5V0QwY1hldit1bjFsYnpiYVNoc1RrZzFROWpuZitHT2ZlK29xR1prMHBDcDRDM1ZQOHBDSXRQN3hMWFVqa3Z1RVNhRjN1WEU1VW5TLzhoaUVnMzU4eVVPSWNPNnNZdEVwSWNITm9CT2VmVlJidU96KzljZUdQdERmVG95QmdMY3pJUW9HeTcyVWhpTXp5eFlhdW12c3pwMUY0NW1Bb29Nb3pCU2szSVZEVlRiY042NFlQdXU2UTRiOWJCazNzejN6bzZONTJtZ0tIcjRXaHFZbU1tbFBmNVlSWDBGRncwbUh1TWNzQlRIMGtNWHlOSElJV05lNXQyOWo0eEpodCtFQ2RUaEtuUTl2OEFUK2I1aWt3MGJqRVdSbWFIQmhGMlJXR1M5NU1VZHh5cWpESlRXVVlsT2JtcDZOSG9VTFJLSGdrZ0szcHBWWEUvd1o5bGNLSXpqWTNYMmtvNXJIT1EzdDBuaC9Ld25USUFPUVNldWo0c0k1cGtPdEgwa053MUlIWE1NZzdkYytkeWNmYUplbGQwUFptZU9LMHFpQnNNVnJVNnhxV0dVelZ0ZGNkRFdFRkFXMVZndmYwaTMxM1hpeG1ZMkpvUXZBNnY3VlM3dFNScGl3MXhqa1FnSG1LU0lyanYzd0F5MnFEYVhGRTBrU0tCbEJsdXVEOFZoY3JaN0V4czIxMGtlQndEWXRMZGhQYkpQejJoRXhsSHRjZHlQajBtdzVXdDV4WG42cldVWGdYaFNpdWNHQ2xkNGNyTFJwaXdUcWVLcTBiL0dLcjJUS0MranVVN2p2STN3VFVpa2dUZE1GRE10VDNCRDNtSnd1K0FDc0pKSXZ4WlFwSTNQVWxZR0d6MlZiMzRMOHFTU3ZZbFlUMEdZSjRYOUcrR2dpOWRVc05oUmxXYkVIVWlJOVREdmdvK202RlFiOWRQZnBxa2VyODF5MnNqOWJMWFBLWXJZRHhnNWY2d2hkbHVWZ283Yk5xT09TSmVWLzRQMkVTU2ZRbTFDT290UnVHZ3pPeTBNTWNnUVVldjFVUGVTN0V1ZGg2citUVDN5dE5zNDh0dmloeWJaRE03K2xMcEFNdnB6SEFiRG9zcTdRZnBIMDRINGVJSEtqLzcvTXA0ZGRqN2J3WDd4QUY5TVRFK0dYSGxPWDhiVWswYit1VnR4aTdSc2pwRkhxdTNhTi81QUw3THRIMlBzUDNwMktDRnR3WHArZDF0ZGlNcWp4Z01heXFYM21tQjF3cDFIb2c2MnJIRVkwTDV5RVlqZE5vMW1PN0ZldmtUMjl6SEROeFBrZzMvRDJwZDd4KzcrbDlhYVJlaWYyVW14ZVNmTXdVVjhHWTVzN3MxU0ZNSDNDbDgxeUJPSXNNQ3pZbnVxaHJTNDRDalpSOVMvQXpleERDWkdRbkw0R3JtM2d6Ym5oSlpwSS91OFRQZHpoUXRmYXd2N2dzdFMxLzltWXo4dlRwNjVUYm54ZnlodEpibVE2aUpzL0xtVjZ6Kyt3YTF5UkpuaUJRaFNsVWZBQlJwV1NZU0M1VXIvS3RHTGV6WmVYRnZlb1A5U0thVG5UUWR4Z2g2U2cxTVJTRDlHaElhV0hnOHpoK1UvbEpKbnlqYVNrdWdZT0V6QkZqU2xUcUgyeU9QZHQvN2h1VzU4VU9IZ2tmSmlqNU41SmR4SzdhM2FwQ2ZvNXBYdjlIRC9TbTc1VENndVBQRDc2ZjMxdUJPZ3hCdklFYzRzMTAyS1MzaHJ6M1JxZDRqTGs2Q3ZVUC94ZjRQZlhDVkRaTHVXRFlXQ2IrN0ovOGJ2clRHcExOWndqeUk1MllCRy9RRmY5SUxrRHZIdHNzSC8vV240ZWVkM3FWZU1aMU5TUmExdS9nZjZBR1l2YUJrYjFDbU4ySmREamh3SStYbytJbER3R2JwQzZxR3d2SENjSG5WWTEwV1o5N1JLTlpUYytLaGdTVDNoQVhRZW8xV1FEK3VIR3c1dEZtOUxiUDR1Qklqb2tQNVVFczlJdFVUUFFpSWdhNFRWN01JZk1qcmtKaU1JeTFEUzZkVE9TZHVxYlNnTkE0ZHErUFpYUk9LQXBYWFZWMGZRejR5bWxaNnhROVJjQTloeHljYjFFYlFZT05pRit1RXVhbk1HT1Y1d1lrQnpIclR3TG1FQ2NVL2NXVXliemdNKzBSbXNrRTBNSVkrYmQrZ1VKeXVhUHA3c2UzampvWGprOUdXMjBKTUhSZjBxVU1UTHU2d0ZtOTExVjEremswclNnWXRSSVFaaWh3Zkxncm1leC9mb3liYTE0My95RzgxQmk5NDR0djNROC83cUNLMjhCdkdZQ0o3MTJIQkJpOUNaekpYQUtkbm9oNHp3d2VZN3pJU2pkbmFZOGplOG5hRVRaSFFhTzc3bWk0Umd0YUJXRldGK2w1Z3BmZTMvSHNOKzlzdzh4TGROdFgvT3BaVGtOSmM5ZHdKTHZOaFZhU1U2QTZ6anA0ejQ1RW5zZEppOE1IblpzZmtCemF5b3VRbmh5NFZGeUNrandaKzVFakxNMFBodTFwMGtoYS81UmV5eGczM1ZzSVZHeGlVQ2owWUxROUtmSXltN2NtNGN0NHZhdHpDc2dpQmhmUG9NSjRLdWswNnNSN0ZXZ1B0ZlFITHY5NTFDQ3ByUlp2empOUUJGTVlsdldRWENJcm5Cayt4cHV3KzJ5SmlZbU9iUHlpcDJhQTE2SlJmT3c1RmlvZE1XNDVHaXg4SHozeVRUSS9RSmorMlkzOUMwK0VtSktKaVdOOHkySjFkM1cvTHVrMjU1TzUwRENaUFFWVTFXbGtBWHFnaWRld2I0SGtnMXVzWGwyakZNaE9YSlZqRDJsa3Q5UXhRSThvengvL0ZBZ1NMVnhycHhSMUgxY0hLYmRyV2NVU0k3RXpFU1pYU05IaWk5aDA5RWRJcUdkT1BCTERKalRYWjlmT1FMbjRyb04vNU1qMVR2VTR3TmZTM3FzdnJIMGtNdkh6a3I0VE9KMnVpbFpJVmtKZVRhNUU1RnBMazRVZDlZck1Yd2pjVitrcFFscmtiQ2ZSQnY3NytXTnBaaS9pZTVQc1l2bStZS1k1dDlNOEVlNE85S0xRU0NSL0tYbjB5Q1FFbStWTjBFUkE0VTkrVjZmZzRWVmpzNFUzUVBYTzU4c2hQUDZrYUFhUzFiQnBtVEZkbnZEZzUyTzR6dk5HY01BTVNkN25iajJoVW1yQXRDM3lzS21mVlFPejBqMzl2NVYyaEV0b2V0SkZBWk5VY1VSZFBTRS9weDB2dUdrK2I3ZFZDeEMrMG9GRitxR2VYUzV4WWVVWVRLREpiRkw4YkkydERsc05BZGh4QndFcGNkb1FidjZiQ2tVNmM1M3dVVkg2VllmbnBBaW1mZXdJc3RqQ3U3Qzc2ZkhQQXZ5U05Wc3V4OENzU1JJSnA4ak14VHJpeTNwZlFZSFc1VENWMmpGeTBMbW03V1V3VXdSOGRpYituR29URHk0R3NVeDZHZm9qUGxtelFCUk52VVNkU0QzT1JubzNhVW5mTkRPYWVISVB3aDZYQ1k1SVBUcTBGODhWT2RUcUJMT1E5Ym53TzJrVzEwbkM2Q0N6dGNGNzNhSG82SXNYZENPVDhtek03QkhQcFZ4NTc1c2s5NUY5VHRacEpoeWpacU5XQTJ1T2h3UnYwZjE3Vk1TY0JXazNFOFdYQmpqU2RmS0JUNXBIMVJ3QWc2dW1JMFdINGpIVExyS3E4UzBQekd2dHozK3JrZFhiZ1hBeGVCRHQwbE1JbjVTOHYxVjZxODJ6ZmovZTVRUGJoam1FZDd4T0xORFAwZVo2b1FoSDV4T2tTQUtDeEhSNzcwTGo5U3lXNlVsRFB5VDVWTHNaendBajlnMGZCa0NiT0Z3KzdWbWNiaTNQRFJkTG96ZW5weW5NNVd1QTlyZll4TU5WK1hoWHQ1R1dTZHFDckdXRDJRd2dTMWtxV0dGUW1oZEF5NVlYUGNkb0p3Ri9Jci80ajFLMTVlQkxSTXJMV3NiWWxHZC91N2k2c2dSQXdrMFFQbXY2aXVxeDBqTU9ubVEwdWxYMmVGTVk2OGpQOUIvWXA2enhQZzNnS2ZPSzRoTllaY2pUUzRyVUt6eDhsMmU1ZFIzaEF2b2ZFeTRGVU4rYnRJUTk1Q21wdUUxUUluQXYxaFh3Q3lmMytwam1LSE0yOUFUanF1Nm5lSFhKdC80OGNlczBiQmRRSHJEc0l2L0NndHBiYUNSbzRDZWFmLzFsa0cyRENyY1R1SWQwR3ZIRzNiZTZHQS9vTDVwcXlianN6d2hVQzVUVlc5TFM1blNrcU1Ccm1Wb0RpQ3ppeFZKTmphdjNaU2E5bGY0akd3OWk4OUU3Y21udE5PdDdSN0FwU3E5VGtOVUl4cWpEWVgrbjNuWjFNY0dUQ2VWRFNIL0ZRT2twMVQrWmR6a3psT2ZPZGVWNlNpZ1pvSk0za1BmTEljNlQydEZoejJHZjU3aWdNaXNRQUI5QnhudmhuQTdvYlpIRUdnWGJoRXhmNk5oYXlzSkZ4Y216b2g1Wi9RZDZ4MDRkMU1WRE9oWVVpUXI4M05RTFVRZXA5VkJ2ZTE1bHhrYUdHRXkwS0tMUVVhNlZZY1lOUHhJbWV1N1ptOGhDbVBkWmt2enhCWmhKdXVJUDNseHJKVGV6L3hEdkNrUTRVZG5tREZ1cHo2OVcrNjZtUXBxK00wTGY5VHZweGFRcGNrTkRCTGJ1ZjFUVHEreFZ2ME9XL1ZiMGM2S3BxaUU4d0d5aUdtRTUwK1FPU0hDaXVoL3JqNmdlaTlnTnV0b2N4SzZ2WWhJSW85OC9lRWJGTkNROUpYd2RzN2IveDFTMXhESDZJMjN0bnh3SFNhYmpNYk9kTzFqckNReWh1K3BnaGN0djVjRitoOEVwR0ZoYjMycG53VmoxYmlCMVA2SXl6V1ZoR3VrZDkzdWZtVlFqTGQvN0JTaUk4eEh0Ymd6cTVVYk1nTFZSVURQNjFJRG95NGI0K0d2dXk5M01FRVNjYmpSN0J3cXA4MEVLVXRWeExtK3FXYmsyTG1pTk1NV2RiNW1jc0d5eUdHdVpTQ0lTZXdwUGV0WG03SFlad2hoVzAwT3hSRVBvUmdubFY2b205cGhaYlBVcjZFSEFlR2lXclZnRFNBcEFhaXJtZmQ1NEhUN3FYbjBlRkxJL1FTZUFOMGgvcVpLc0JkRWk5TXB6djVVaExzQTJPQmVONXNobkxjbmE4QkREem1aRTlOMkIrczFOczFKanU5S0RVaGNYOUFlYnNYK2tTK2sxbWluSVo4TEZkMGhHdDFFc2llY24rMmx6aFhZeWZEQkpxMk0yYjdNRS9iWmdnbnZuanJsOXl5RWd3N2daMmd6U2NjdFpaNFptZmFmTEl2VzY4SEhwMURTQXg2dnZxNTBhYjl0MkVER2loWWVma29GZ3JpWW9RZHppeDZLNGRDSDlYVHN0OVVDM1FtNkZoV215VlRWeUE0dXA0Mml0MU9FVldsYVJpZExRSGZOWUZNMUlsTENrTDFnVnUwK3JQWmV2aUF1US8wcXZRQ2poMjhlMGFPQWFlMDhtZEdUUnhaVFYxd21EU0dFcXdmdmZ2RDlEWmYyUU1lZzc1UDRPOFlFWXVOYWVjYUtoMFlwcVBxTVhLKzBGZVdFejdVNWlGeGtHVjhTQ3VIVU1PVjRTdVVtWHhDMlBWWlFKdDdaYXFZMjY2QmY4aFZZS05sZDd0dVhsZ1RUVnpncEFZVWRrVmdVZGRGenk0KzcvVjZ1T3RxKzUyUmtFczhoUVRhUkFKRnJtbHBXRm5tck5LVVZmeTBIVFg0aXllNWlzNmRrR0xCK1lGdzVJSXRKQTI3UFkwWU5HaTZMRnlJMVVqZkVnSjF6elZGTzhoSDROUXBSdVN3L0I5bmQwbndVN2tlcHorUThsWW5HVFZUcXhBbCtDTTlIenB4ckZ5Q1RqdzJ6RDNlOStCcU5BSWxtZDdQeE9DNWRhbXFIUnRrVW5XWmJmS3kzckNjUE9JODFmamJFZisyeVlDZjV5MGhvd2xtRStrT3FBaTNVYWlqRVJEeFNTMGY3TkRWcnE5MFVMdWovWHh0KzlSdUQ4ZXBFU091OTVIOTZLZGJaSVk2VCswdjcwZ3c3UmtSZUNPZ0gwdEFRQ3FvKzZnVVZ0SjBvSVNkMXFzc2p6VnZ6cjF6a0U4RHpCcDBWd2xQeTlzK3Q5SE0vWklvZjV5U2FLSDAyNVZEeTMrSHljK2J1NjJmWG96cVZqVGNDRW9ZVDUxTGovZFpMa2Z6VmxXR2xkZE05aHpXQUlSRWtnaXFsMTFJVUNCeDFrMzFWUnl3ZklwWjI3OFkvOGFyZGN6SUF4M3BiTlZQNEIzeXRmV2pNRkQxQ2E4K21ZSG5keU5mU3JrKzhtRVdwV1NFUXFQMlV0YjF6NEwyYmsybXcvWkV3L0d1VnI5WVJ5bFZsRlJSS0FpaDZFUGdycE9pdE9nZEI3NFc3TTU1TStCTjh4TWZRWWFzL1g3NllXTXVHRVNCRUlodzM1QUQ0V0MxK29MWXAzUVVwWWRBVGg4bTNxK1NCZ3ZpRWFUQ0o3cnNudzVLelhYZG1oSnZQdFhlM2JBM2J4WUZMc1F0ZGVJQmpoMmFRblFSdHRDbGpyVHdCdk16Z2dDY2w0RHk4M25valpzZ3MvR21PeEczT240UWNpckVxN2JOU2wzOFRydndwSVNGU2lOS3VSODNaSDdtT05aWHJwaUFrUEJrTXFxRE01ZUx3aDRFd0VHQjdpRFY1SCtRaDFnaHpDcHRrc3dKazRnYVVqNnBsckxlYVhIWXo1dEwvODQxbTloZjErWHRBZm42RmJWT29jYXBOd1FxaGxHeWtoSENPSHJUSlorRGkxOVhGSERtUC9QaWc2VFR6Rm1sVXJ2Mk8wQlhINDExQWVGZkRyTEY3aUliRG4vM2czTW91cHdFbVNKUzVMZVo2YXlLY2llTlJOMFNjcENXMTdhbFNkNHRDMitCNEx5eXp1WUJNZDQ4KzRCd2VMYTJ5T3FHUWtoRHBDMXhkVHV3b25oQVFRK1YzSzd1bE9LbGRGN0o3MktZekpHejloOEN3VjRkZ2lCWDVvNWV0eWNScUQ4OEFCT3cwMmVUdjVCVDZ4ckJDMlhjRnVVY2oxQmViK0pUWStDckRjcWdDRzhjUmljS2lUZ0M1VTR3ZXl4UXNWOGVQWXBYUHVVVFk3TWVIRmcwVWNJRG5nZnptRXdQSzJ6WUVlNHp3QlpYZ0w4elJjTUttb0l4YmluNnV1RzN6M0hGTndONFVjR1dLMDFUT093bE54R1phT01UNzF0Ry94Ti82NHhuTVFzYmYxdG9IWEdDUFVpTncwd3N6OUhBbXJQOU1CUG5GdE9MWnRZdFVkSGFGcmhBSERmRVNmbVQxVEphVVFBMGdWUVl2UFJMeFFqMnBNZWE1b21EQjdZUGtvYTF4Nk96eFpmYllpMFVaZE9Ec29hbkdvNWxMMHNqOTJNakNkMWlZTHFYQ0RIZ1ljcVYyL0JEbVFvSVhIOVVQUjRFY3V6blptMk1ybVhMT3ErNWt5VGRFSUYxZ2laQnEwSk44SUJXTVZ3S1YrS1FNUWwydnRReVB2Z2o1cnYwMUNhSU5nNDRCTFQ1cmJTMFd4Z0gwUFk3VHo0NCtTWFYvUmh1b3FUODZ5UENobXZyUGk5VTJMSmdscjJJY3RVT1BPUmsycjQvclRvRlgvaCtCbmMxS2tmOTBzSHhMcHhHRlJZeU9BNXB0M21TVzlPaE5xVWM5U0EyWHdxTmFGTy91NDgvL042OUI5L3U4T2pzQlNBTGd6bWJlS0l2eEkxdEpyRFJ6c1NoNHBNQUJyOXNWMnZjRm16UVR5aXNLNks4T0lHSktiT2hySUlLM1BiQXZnUGM5VUc3OHlFMXpoMXFJME9hTDUvT0JzU3RzZ05iNTVuclpZZWNQdmNtUkFUOXh1NmRHd09QeHpuYk9TT3p1OVlPNkZHc3JwNW0yNmxsQzIrZDgrWElvMHh2V1lqcGVFWGdKRERsUzU4SWdiSVlFK0hoVW84ekVoTjJTZXluM1B2TE5qNjNtZHVKNS9TeDE2QmNlYXNPdkdic2crWmw4Zng5R0ZWL2kxcFJIWFgvdGZRZ2lSeXVQWXBHbjhjZ05Ic0dUWDZqN0YvU3FwdDhNTE03THl0c1oxOEYzOGJQOHI0V0FjNlZTcDZsanR6S2w3NWJLYWp0V3UxSUxUZGhyNHVsYkg3ZkpTUUxYUDBJdDNoanZTRkY5MHE2Z3BsbVM2NWMwc21rUmtabDUzREYxUjRJWEFQbmtacnRMeWJtR21lcXd6T3NLaUVZdjZudkZPL2J4NGhSWTR0SDB4TGlSUmhqL3c1L0tiYWk2c3lHTmhLNTZ0M2UrN2czUjZyTDExbDlMZFovdHk4S2pPTFNGTDFxOEdLU1h6bk5oa3hseks3blN3NURObzJmQW4ydE91MlR3OEVTRkRpdGRieFhBR3kyN2NmdnlxcWJ1dTJXZ0pTdFVGQXNxdC9aWVBzaFV6bWh4UzhIZG53SzVYUlBScXBSZXVqTS9IT2Nhd0IrMTZDRVN4SzRrL2hFVUZRL0o3c2R6aS9tQXpoaTFuNE5DSFFnYXJrTjdXMWRKUVRFV1A5ME4zeXlmbWQ0R2NWOTZQTFV5RzNVdzJLblVtOUs4V2tHakg2VmtrYmMzMUVZWlVYNFBiaXlna3JPZHB5aXF4WE9NVXdJZS9jSXV1VUdDSWk2U21Sb2gyUmIvREZnYm8xblYxVVR4L3VFQmlxVEJ6STk2Z1p5aFprK3Z4T3I3M0tiT0c1c2pFbVh3ZnNpQldTTkoyNE1hWXFnSjRwdW9PN040V3ZUTGlnRElTZTlWL3dnbGpOM08ra3dMNEpXZEEyVUJIY2s5ck54UGc5eHpLL1g2RDEwOUdDaGI2SVd5MTAxaDNtM0pCOXZUdVp2YkliRG1iSUJGNTJBaWJnNmxmSndqb2l0b1hoT0lOL3FlZ2lZUW8zMjBwYWNYVWFFVmV0eUZ3SlJQQ2dpZUc4TlFXVzFTdzFUazFoaXhISUpBcVRvL0hPZVZ1Y3hyNkJpMzdkUDZyeWgvNnNEb3NIS2lKenBGWXJTVUZCQWJROS9xMnArV3ZtelN4cWJBbk1rS3plL2YvR2ZUTEdFSGV6SEVsc1ZHVlVHU0ZqdjVqZVhWRGdzQUxjUk5qdTdIUUZQVlRZbWFsRnB1WlV1U2ZaVG4vbzVSUVNsalc0MzZrVnN3MEhXZEdzMSt2b1hCY0tkR0NVRms3amhibzJkcGNJWFhpSGVEMEM3TEFyOEs4OTFpUkxEektVK0ZVNUpjWTRBdzk3WVp0TnJiWURaMjlWRWlEbjdaRDRFUFViOXh4c3lBQVNHWGJLYnZsd2tiMHV6QmV0eEx0L2xHUWlySGZDNGFhYVhPNjlXMnFGcnErTThZcWhHNmttTENIYlBKMGhFbWJNS2FFdTdqbnRxS1F0UW1CSzZVZmVYNktmbkZRckhQUFc1Z3ZEZU5KZS9BYk83L01TckxqTUhlRE9jRnNEc1JETXFaRCttajRud05oVGNjVTVNNFoycmM2ZHR6QzgwVUwxYXZmNnVMMThmTXhSQm9QRDlJMXpyMlZLRGpWZmRRYnozRW9ZRlg2UFJVOHVFbFdRejVzaXM4YTFzVzFEeC93TEN0SUhudTlWbjhJL2tNWUZBVWxsZG9SYVl0TjI4aHMxY21QRDFGT2xTWVVYb25ZS1djaWJ6dXJYVk1yQ0R3ekVEUnNBdjFUTjFwVDRxMk9vL3RxY002N2U3ZWhRblJQZHVRY0xvekREU3FOeDJqaUxReGx6bzE1Rm40MnpjT1dKRGRPVm5aQW1obVhNT0hUTittMnlGdFRwT0VUOWdnNk5UaEl6REVhTTE3eGpCTzlPMDRWTXRzZDg0Zytvd1RGbmVDeFc3d2IvVHByL2VuZzlNSjNyWFRHQmRrL2xXWnE1ZGw1RGNvYlBjRFpIemVmNWdaTkpsc0JFc1hYUTFLWHR4UisyeEFrR3JGY3lLVFMvS0t2SklacXVNSGZEbGliOTM4amxmT1JhMk5IQndkSFZHem1ZRE8yY0hVeWtYdUR3VnN6a0NSbVVFY1prWkVHL0VtR0VOenB1NlNCMHNMdGduSlA4bCtZMHp0a3g2SkgwVDcvMmNSY01lRVFNbi9ySG8vdlJHMEtxcFlJNHpvZ05VbWVKY2lWTzhFZnNPYXlIQk13a0cvRFZET1VNM1ROTTVva25LV0wwTCs5V1ZRSm9KK3RmZEYwMklSS1pEakZoR1hGS0RnczlBa093VDhRejRhTDQ0NFFLL3NqMkNUY1MvSXBaWXJWNTYzSnkvQXJxQlVhSlBrcVJJY28zRW5PVVZVUVJhekF3T09yUjlETUlVL29QdlJaYTRHVk1kNlZuMlc0anhtNnl0MzVGSytDcDlPRFBueUR0THRaWUd1SDJqbDZsdE1FYSs3emgzMEpmcG5VZkYwRmlZQllrQVBhQ2R3Z1JsTUhyOTNRdzdHVCtaekNZcVh6emVqZFBrRStMZEJPT1ordkJMSElHRHBFRHltaFp5c3JLTUM2Qk5VZCtmcXo3QThxb09YRmYrMFo0YVZPTEliRS95a1R3UE82eXNxcnVTU2tkeFA1OVBrS1M5UjhWVHc0WUNGUzE1dGErQ1UreW9wOThLdDgwUXlWYTJ6KzFPME4xNHduaHNWSkJ1Si9jaXBxUGZyRGZoZE9JOFZJN29jazhUUmZ1enVHNHpDZ0ZkaytKdVQrbkNTc2trTERHWEc5TmFHZ0Q2WjJ3cGxUUzFXNkNGZWxBNFJ2Rm85Yk1yc04zcDZ4dnp5MHIxUnV6K1ZlVEpzUTVqa2tjU3JiTmc1R2RZTHhhU05JRXVyb1V4c0k5Y0FDbnpFeGNBaW5tU2NvV0RkWUJkaTFJOGEvZ1hUL0VlbUlDL2I4NDFrTXBBMS84eFY4TzZWNjZ1N08yd1c2S0VJR1YzcUJnYTNCalhydWZZckFOczBaNHNDSEJrNmsxMC9LcmxORHdNaWoyK0IyTEdKaXljWExYNkJQWXgrQndlN0F2TzY3bUI3YUtBN3dSSU9SSng3MElmVTl3V2c1aWI4SlZYWE9odmNvK3BXWS9DditiazBsUmtObkpSNUQ3eUkzeExUQXBFWVcva1VhTzJSeWNDeWJkZUc2U0cvTE5DaDNOR1QwcW8vdWxxd3hkcW9uSW80NTZERzBGNzVFY3lQNGRMbzYyRm53TDZyT1d4ZEVoM296Z1BOc2hpRytSNWdOdlpNc01qTVZHYUZBWVNUSnhHcit1eldSV0lHUE5kWkhJby95UkxrMDFzei9BMjE4Z3lTa3RvaHVGQVJCc2QvSkN2TU5wNG1iejYwQ1lFUjQxYWZVOWEzcGsxa01Zdkdrb3NOc1VGbVVWL1RLdmQ2TVdxVDI5YjUvcFJienhwL1VFMmpuZ3NCMnVyUTZKNWR3T1NQdWM0SlBPRFF2TWl4V3RldjVoSUZ5TnFNQ05TS3YwQ3l0TkJ5clRvWEtKUVhxZk11bG9BVnRIcU9aR3o1MlEvb3Fqb1MyV0l2czNUU3RXZG1jSlFIcEtCRHE1N0hlVXM2ei9OODJxSzNoMzhuZE0wMTA4TVFsakd6OHc2bFZ0dDVLeWxPMWdwK01GYm9KZTlLSkdBT0ZKWEFFcFFLU3ZWdWFVSCtaeEJjQStqQ291U3VBcUFzZW5sVkRnakZmOFVkVC9wQnhlUGtnd3FOK2NpbVJIRGNmekFZeDRUbWVhTExRUFE2MVAvRUJxWGRqcHVuR1l5aW5LeldTV2o5SWFNSDMzQUpLN1AwZWQ1bG1GeHBzUXNoTzFhOVFxRmNOVEh5YXhzVFF2dlFwVjFubDFaR3ZiTlNETHYyMGVuQXBGUnBWZ21iQjREQmRCeHpvY3FVOTZOTFcvSnM1bExDMXNoalU4QU4veUNXR25qeThuaWlDV2Y4MVZUcnJPTS9WbWtPa1lEN0FHOGcvVDMwMzJlaGQ4eXFaRjhVSzhMMGVpZmdrUzBrVDJJUWVWR0dHTTRMRmIvN210VGpmOVFtOU8zUkMwZ29USFhTd0VVQThneWYvcEUzU3kyM2l6SGxxN3E2R05vb2pid2RYWFZJSWZrU1phRHZ6cWNVVFdZKzMvWGdxZ2RUdldaL1VTOWtJNUhuUWxEcHAxb0tDZ3BFMkFQeHQ3dE9NWmRWalFvekdOeXNaV25vUlMyMXVtOFJ2Y1lsTE1vbXExdFVvbTF1ZndSd3haK2JqNlFUWVlydU1pb1k1RWYwdVlsWHhGa01HTjdmVkhiTzMvS0xoMllBZ1NNdVVhdUVxdTFLRWxmdUpJY252blBiMDcrTElHMkg2dHFLTHRTNEE1QjByKyt5SEZnSGFwRlZSN202UytjL0FROFVXTSt1VnV0ZCtPOTRYZVl2dThMaDZSZklQYmx1RldMR2ZOdW9uOEtHVWZuWFdCcmdlc2V2ZHhwUVBtbDZDMC9zYzcyV05NU05PVkRadjJxbVpXUjRyMUN3dThMVGV2YkUzbzhCa08xN05SZU85cmV5emJpU0poVHptbVZJNWhhNzdPbDRMSlpBLzYvSUV6MktBbkcrVEJkQ3ZWV1dyT0JCbVFBNHcyaS85ZjVwVlFDQUV6VG9FdEhDMExCLzVkSVV4d2ZoSHdjTlBTak5HNWtPNXBSdkhSZldFRXl6d3F0Vy9xdm1YbUV2SXlaWjRhYm5CazIyUGpmRURIa2Z5L2wrY05qa25FOWNRUmJXczFHc0d0M0pPbGpKYW9CemNyQzNQWkc4czRIVEdQS0lteUlsM1Fkamt1ZmdLVUR2ZllOTkgxaWlYYmVoV0lmZjJSQkVDL2VPT1AyR2I3MDRURFpIRDVaeGF1UGcxWUx4TWVTMjhtUUdZemsxektsSXhBajI5OEYrY2Z4VG93TkMycjdYOVc3RGsyUGd2V0lLOFVuaFQzOXlSV2xieU1vNm44clJNZTlTKzZVNk0vU0xudjZZdlI1Qkx2TWhGQ05xWUEwd2t0WUhwemhZS1NqZzJlTGJ0RnZNV29YZ2xvSnl6QUp6WmJUcEhNRlYzM2xCLzhzRUJiUFBPK3RSZGpqeW5DdHF6RDFEcU5uQ3M0WUg4UWZkLzA0ZGJud1lWMHhTTTlVaVJYaFlJbUtMY0kyVVN5NnVvK3lSYldEYm03ZlNOcDZqUmk4U0FhbW9rUm1pcTh2eXNGZC9lME1DWm5YbmVDclJhOVBwcDljOHEvRDl6L1lxUjVRaWNpL1RWN0lrRlE3NjhLUXRXNnFaZ2lsRDZHclVFZGhIdVEvTkNlbzJndXhyQ1dlVmFnV2UwdUZtOEpZVDc0U0V0N1ZOTjRkdVNzZ1IvL2UySlNtTnRQMUl3ZGk5QXVIK2M2U2NkWnBkcGxQTGwwMDBMSlM4REdvQ2RoVFpFMXJBUHhIaldJay82S25xMWN4OGNhR2dMMVJwQk4yeTVaVGcxcFcyTUVHY2hFNjJqT1A4MGp4alpySXVCeEtBaTEyeG53cFZFb29ENXczRnNlK0JTLzRuK200djBwVVR1a3phMEVIWk5qZDFRTHZ2NzVRdklxaDJRMk5jcHk2U09mT1dEWEQzVlNpaGRocVliUlVsTkZtYW9qNG5rdjVwU2VJdmdPaTQxQ2lPN2JrdVdEeUhMWW1SMTlVUUN1NHNSbGhHdUhnU1pla2thY2JyRjJVTmhUVm5tV0NUTFdqcG9ZSlorMkE0RGV6WENRdDVtMXAwV1N6L1VPUkNpdTZ1dmxlZmFlUkw2eWQ5ZFVNaitJSEQxc1FtVEsvdE5kTS8wSXNGeWRXZGF6R2w4VEZrbzc5ZmZaR3hNM204Q0R5aGtJODJ6cnpzRzFZYU9zVGtXeFM3azNkY2RoeFZBRTRaV1B4dzhKRzVpZ015a2E3NHN6ZE83UDZpeFlYTGMvaEliT0NFSjJncXhUb2gzOGZBWHZNSmN6VHBuNEY5YTBqMFNSaHVLUVhHQUNtSmdtQ2YrY1FOSHRMRWV1WUhBUXlZd1k4TWdiVU5qYy9zbjJ1V3J3cUl3YUcrSmdPeHBhNU9BcDFJeUUrOXBCeTZuVzV4RGNmZXFRcEVBdVA5TDlNbXNCeEE4T3JWL3daVmhJMmI1bGI0Z2VHWUJmRUdMRTk0dlFxbE5uNVVUK3A1aW1xSWRLWmZxM01hdGNnTStUbHgvQXBzYWZhc2IrT0F5YUNtcUo3Q0xSQllTcHRydE9oK1c4dk1WTjY2RzJxR2JvV24vV2dBWVJ0bm9RQUIxeG51U2Y1dGZndU1DNWRZLzhOZGJuZE9JUldsRHBYd3p2c1RNNHVZQzBJamZCUWJxSWg4VkdlblFzZW5xdDFLbGJtdXNMeGpkNkU2S0FZVXJIM3B3UjhYSDhOM3hzM1ZVcVRpTjlWRnFGMXpHVWN6dW9YOTJ3dWpTMHp5RlRuRWllZjg1ZC9YZFdGUUY0MWlXcStkZTc2V3dPbUFuVjRVdEk2bFYraUsyaGpCaEpleVNDdnJRS2o5YSt0L2UvRk90c3NVVDFYRWJkc212WFZqSVEyNUEyVzEzZEZUaUVzLzBNZEhwdjFGUlZ4TUpYdjZUVHFYTGVUUXVTRkJ5RTl2WWlWUGhHUjBOOUpTbVFxZXdEeFd5d01YbjYyU2JQY0pzVjRQRkl1WmJhR3NyWlFhd2VBeDEyZW9zZzFsVVJaSmxYY0FFMXdyUVhnSVR4cDIya2YySVNlSDZsVEdUV2FuZEprOG5aRVRYRDVCVng4d3lZVXlXNW9HbjRJa1R4MWJFc2w5ZjVxUUFhWVVveU1aWjZKY0xJTnBYUE5PeW5EV1VIdDVkVTlIdExsZzVucGxBRUFLek05UHZGeVYxNUtQaVZIeTZwaVFLU2dZaHdYUUl6YmJUNFhSYmFEdGlaQlora05IQURhTHpyZDRtOUZ2dzBYWTFUemg2WFoxenhHUUcwbmlLRGttcEU2OG9MamZEak1nRGp6VmtLZ0pjUllNWVI0MlhXN0hONFcrQ2NtM1BqcUFlRG9Mait0VldlTEdxTW81ZHljbGpCQnhxemlTY055YVVLU1ZtS21QSG5RZWw3Wkpla2VZdXlOcjQ5R21IdWRibEo2Q1VPNU9qVVg5Mks5dzRFdXRxOWNwM3VWUmhXWjZiZW9YcUtqMWVOS29tM2hUUkdWUCtqbVJrLysvYnRzRzRvV2Y4UUlqSGFCVmtXNTdHV0tkb2docm9KeVowRXIvYWhUbllRN1pXSFIveHdBengxbHZTQXVlQllkcTRMM1lTWXlHQWdGQ3lZc243RlFjK2Z4VnFwUFBlVEdLZVd6S0xaUHEvcWN5SFlpeUY5SWpRTE1Jdk5HOFU5RTBXbUgzVDlsTzMrWEZMRE9VbFRseXN5dU1vTXpvbVNtYlBkWTRrd051UVZnbUxSYXR1T0l4SzFLeW1CUWxlR1hzVzNCSElFNENCNTZrY2VIdCt5d0YxVXJFRG95UndjaTZrWlU5K1RIZXUzeTZTSTZyRktwNDdwa2lvK1g5N3Ywbk5jU2xzK0x6RVY4OUpuM0tGYm44bkJQYWF5REcrM2lFUlIrU3BuN3FwZlpHRVJPUVRQR0RUdWxSUDU0WUhGK1RSQzg0QzNGV1hzWlZ6eDh2NXdzZi9wb0w0MklnajQwNGJXTjdIdzEwd1pETjZMTzlkWTBzM2s0MUI3SG90cFhpMStRWDBvUWFNVlhvYVVjOWI5dFpBZVBFYzBVSVhUUUlyazF6b0hmYjJYRjYxUmVVOGlLTGNuQytodXdtZldGRTZhalhCUzk0bVhYUHVVa2JLRk5IdnRwb0I3UjI1VjZjUTdzNHByN1FmRmpnQlNtdjgvZEFCWUJXVWtKMVJUQ1BuSTRZVUpidTRCaGJyb1pHVG9iWTdMTUJiVldxUGZIY29WVEFJUWFRaCtnUFlwVytMdXVHQThId3VsdjE3L0d3LzBDSUpoUG9adVR2K3NFM0ZFRlZVQWNRWDBrV01aVWR2YkpWSnBqMWc0b2JpYjlkcHdOa1NFVlFTanNXZi9zdTREeGg0NmgzZ1pjeHpaT3VjUWFwZE83blQ4ZzhMN3VYMHZwZ2Z5OGVwT0l1S2NtUVFndC81ZXZ0M2lacWdhSThMYUlWNDNDYnpLcW1wL0l4UWZOR0xKcTY0dWF5Y3h0Z1lvWFdaUzdxaE1CZTR1Z1dsUnBxa24xSVdkZThRK1NBeVNjeXczZFM0NTZra3JNSU02bEZiMG1qNlFDR25waFR1WWVqUmhPeVRsUjNSck1JVXRaOTlEenNpM1F3OGtqUmhYRytGY0o5KzJ0R2JWTU9JY0VTa3ErRndCbWUvOUIvVzVtTnlkWk5tUjdDb08vT0d3UEpGa1JIb1hZR1B5Rks0cDk2aDJpb0Zxa0JPaDFkL04vVXhqMUhSRGhJeGVHT3VNK1EvS29NQjg5elN3TmJXSEFPWlhyNHcvODV4WnB0NnYwUGk2TW51TTNISjFrYUpZUGt0RWVhOU9IL2JxYzFacVJjUXlmdDZFNUtuaE1ISjczQmhHTHFQQ0dtMWJyM0ZoOE1BeHJOT2xKZUorZ3dncDJTeWNJRVl1bUZQSUxJanhmemRFZEkwV2pLdzgzeXRoVFhiLzVvNHdvbXZMdnFPa0U0YWVrSnJhWDBuNFBBYW52RjU3cVlESVJETjJiOU5PaWtvOGFxdnN4MHRjakQyeTVIZHpEdVNzM2VOUWZtMkpCR2U4UUFJaFFOUGlySjc5R3BZb3FCakp5YXdTUCt3b3daVkh3VXZKYytXTzlmMDdYV2k3ZzloSXBTeXluREpPRmw5bnNuaUVrMFVwR2U4SVpONWVSMnNUMlJESGhEUTJSUjVpNVFDSFRUL0c5VzFyaTIwUVU5OHR1Tk9iS3U3VjZLR0F2MHBrR1AyaURZTVhNblJEUVh1YlRBelBkdnZTNGNtaWV4aFNzazZpTzBmRER5QVhHYm13eG0zbGp2WUlKRGlrWUYvdStEN25kUmNoZlU4NFdiWDJ5cFlnMnVmc05INTRad2t0OTdTL3ZlT3h4TGtzMnplbUlxaW1pNXQvNWhnekpwck9mOTNIdHNmcURKcVh5eWpSSGJ5L1o0OWdIN3B0elhkMmdCMXEvK280K21ZZGt2bzVLNUYxd000V0RSVTVUdmk2UlNjdU8vajM5TDRHWGI3L0duYW1hMkc5L2VSM2ZpSlRyWVdyUG1SOHNUblZiaTk3RElMdDAwYzM3ampGeXJYbUltRW01am5jWkNjZmxUcEtIcmtabWNzaGRwRDJsbVMyNS9ZTFBCeHNWam5jNGJvQThVUUlWNHErbUZjcC9XYnk1WDNOb0lZc2Y1aFhnejM0YSt1b25tMUJTZXNnSmJCbEptcHlUNzZaTmxYUkJvTXZaQy9taFM0aGZZN3lTTFFVbjdsanlBODcveU1JZFJMc2x6UE9ZeUxsQzFHYkNteG5hdHVuT3N4OTBGcmtHRFBWZkZZc3YzcUFWaG8ySGFRbm5lb1ROOUc1WGQwdU1wWEhvZklmSTJEbjlHYWYyNnMvK1UvVTFGUjJyL2w2SFViSzBhMCt0MGI2SllFcHF2M28rQWFva29PVFdQMktFZElGckZyNG5Lb3lJK3NDejdFRXRTZXlYOFJHb2xnNHBBSXZPZ3ZvNEM4NWJndzgybUhpcThvSG03a3ozdXQ5Z2pYS0tkK2V3ZGFIUWdxOTZIU1FsM1pRM2p0VC92VTJRY0hVRVRVMmpmY1ZyQTlTZXNQd1VkeUpHa0Nha3F0WTUzR2FlVGY2cVhGd0krTkxhdmNwSkdsd0ZoY0VIQTdEMnliQ2h6QWk2Q3dhQmtUb3NzKzd1MkhEdkwyZC9LeVZKSDNVQ0VOVFJvTEY2YzgzdnN6bmVxMU9aaG02MVNoQVRkbUFwVk9ZOVc1S2dIWFhXbjJ2L25XQ21yeWg0WUM5ODRYVFdUMCsyTjlrcUlRemhPWjhPZmlZVzVoQWtsSEdGeFpzT3ZURnFFdEFXT29Lc21jR3BDanpibDF6Rmx6QlVzRWZ3Z2JFVVBaSDlCOHpJVTZ3VWtjbDEzd0V3K1AxZlgvSUlPWEZ2dHZ0UEVQeGdrTDRvZDkzSmgwcml3L01XeXl0ZEt3Qm9qWUR6RDJZZExuZDR3aE9VdmIzWStaVUE4ZjBIdFhRd1BKYW1RUERrejFQajdmc05qdnB6UldwTE5zZm1zM0w1SmhwRGJ3bEpqbVIyMFVESWNBd0RFTGRTMFhMK2cwWGFsa3plK0xUTmNhNTQ3N00xZkxuOFZNeXJ2VDlEWTZaQjhkNm5XQytyb2VCMEdZUzZyd0tYcEVMZk5OS2tiTmJ0WVM2NHZ0NmdkZm51NThFcFZadTFrWm1COWRtdXg4NHlPUEErczd3a2ZETm8ybzFVSVBrYXI3c2dQbzkrd2xRTndOZ2JQOFBiTmZ5Vk9hSjQ1bEIxMXc1cHk0Y2tUcUtaZ2kxR01sbWdaK3R3YWYyMitjQnhOY1pyVDg5N2g5TG56czc4ZFlURkFERDR3ZjR5UWpKeXVDUU1LQTBtREE4dWRRUUFzR1NoaFB5NzhQRnBLbGZRa01TUG5rSGhoTEJQQWVDWXFZUzRlTFZidGI1MjQzQXNFQ0JnTzlYcEVYQ25NMTJ1ZEU1cmpKakd5dGVoaS9idjFvbEllazhxNURvUVUvZWs3QWxaVlp6UFhHcVdTZTdQTS9tTVpKSlVUOWNCNFB0NmlLbXorWnFMZlF0VDUzalJRd0dNdXcxTGV6b25OY0szVXBtL2JFWlJDUEgzUU45V0dMSjBTWDhkcjdXR0dhSFYwZEd2WVBFbWx1dVFFYzhCWTRXREJHSlFaK2tIeXRsTDlzMFJjZ3h5SnRDNkdYNEZraWs3eGZueXFrYWNqUG91S1IxZlJHdUUzdWpyMEMxN0k4WlNiK0V6U3ZONnBPQVVieERPZnFMbzc1R2J6anFrQmxUS1JKREloZlc2UEtQMjMwRUtqcjNzZU45RHV6dXVoQUp2WjhvalRZLzE5UHZrUTMwbmQxY3A1OHRBOEdFcHYrNDF6UUtjdGhpV2J2OVkxZUo1SEpYbWtoc1d3R0ZLNHhnNDlCYWsrdVR6OVBuOEltMmJxWUo0V1FoV2c4bXkrTU12NXBsNGxZUzlzV0xkdCtUaEhGTlpaS0laMjJldDVWdUxwL3NnbGdPWEQzK0lNQTkxRWxTM25TRkFqcTMzTDNKTXBXWFc3Q1dMNk1Na3dtUkhOVjRNZyt1TG1iQStreXlPdXdjSjltK3ZWM1NKcDZncVZxRkZMN0srdzR5QlM3U2NnOEthQkFFSWRwRjF6ZEY0MlpWeWNCMzA5R25QSDkzanExVmNYaTFudmNST1FRKzFLa1I5eU9uNzR4M3FUVEVTSVdReXFXanJnTTRUSkhNOWNTMGY1Sk9tUmw1b05RdVA0Yk84OFRjamRHaGVCNFBxMk1rVGk3bllEbFQ5ZXl2QnBjQkJQWWNuaEUxQVg2OEU0c1hTaEplNjcrbmtjYS9HL1Q1NXRrOXJlOTFNa0lwYndsa2dOdkNWVnRoWVBrR2I1VWpUV0piUmZMRGY2L0twQVRqaVdzSlBtMWkxQUtGMFp6a2p1bGlOVkthdjZ4NzZLZTY4dmM4K3ViRUxSRkxaVStNSmdpQlRsclY1YjQxR2RKUEV5RHNVSjBxb1Rick9JdXF3aHNVZEJVNHpHVUxidFJrOXFybjFRaWJ5cGtNdEMvRjVXU25PS0szUW5TdnpXNFdmYk1zZzVxb0RRUkVvY25KSVdYdlZGbmxqMGZ2Rlk1QjVCUEROeVJlRE4wUTRCN2JFUGJZUXdvdDdvd0I0OHhWZkoydFh6dEt1czQrTzc5Q2FvdG5ORUdWNEhhVmNJd2t0R1gxWFB3ckZmb0xKUU5kaDllVktDbTFBMmoycVltT2ZrUGJQWldiTUxNZGlHVjJLUnVOQjVnSmpQMm9nVG1Oa0xJa1hvRncyTGZvM1VmbEx1UWhDTWxkQU5LRUx0d25YTDduUThCNGxDV0s1TS9XZ1pFd2QrQjdkLzFmdUpyazZpblhhQTlaNkJjR3JUZ1FhaDlzZDVjdzlnL0NjdWJ3dDlFTlphanQxQ3EzYWdTSEd5dmRpU1hROENiVTZJVmRjQ2lUYXUxK0MzL3dmMnJ6N3pDd3V3MnFsMnRLOVYyOUNKdWJvcVVlaUdmT0Y1cjg2cVNkTCtWV2hraUNKUkxydTVla0V1d1lkS3dWQ1NTWVlQN2pHRi9RczFEZm5WSDMvMWY5L3UrWTJxS2l6cUswVDVOaUp6eUhBWHNZc2M5SS84VzEyQU4rL3lyZEJtVzhMM1JkUDF3bXpPa3NURzM4RDhkZFNHcWxxc2x2bkpLZzBJQ0NDVFNMWW9zV0JSUnhYbWlYclVmbXY5RkNjY3p2S25qcFFGa0lMcmc4TjczQk9kSmhLZTlhcFdvNjFGYnpVU0FwV0EzY2REM1dmT0lHcTNkWVQ2Z2xrMTRUVEFpZW1nWW9DTU0wMGxRMm9sQ205djlpZGJwclpLZUtLZ3ZML25iamlmOGRnNE1acEwxZ3l6K2NIYmVSTDBXYmlLVTNNUEJvUDBEMlJyWm5VUFZuYjdOaFJ2VmhkR1FEbnJkTkRRMjdJeWVkcC9mRnk2RmpzLzlDMUt1UENoai9kZmd1L3BvbVpSd0lGc0kxT01vTHZ6dm1yQXcxeTNaWnIwR0R4Q2F0aE5zZkFSeXB1MWRYSG9mRitKNW1UMmdOVEh1RVk3Zm1RY2tnMUhkdlVsU2llSytCQXlzeXFQdzZYbTVvSXRtTEVYdEs2SXc1RFlwRnBPZDJtbXp2UUtXMy9sZjRkYXdkZWFOZDFDMUoxL01WWjVudFNuaHhhNWVtR0YrM3VuNWFKMkhldE9heThFTXhJdzJMckJrckw2VzFQT0tWQXp1bUM3emtTb0lRRkVmaUtjcVlPQm9rbjhGMTR3SVhYSXJWNWdqL1pyclAvRUFQQTVRMnlzc0x6Q0tGVEp4M1hCYjkycitRSmRONWQ2OEpiaHRRdlB0QlFVUEx3L2FVK3VvSHFIMG9HcEVDTDVsaXBrckIxZzNGUjIxcEVtZFk1ZzdtcDBpd3NxZTR3QlN2YjM0ZCtjUEo2d05ONU1PSDlrWFNMbjQyNWJZOGR0ZW9yNFM0TnRvYlNMVmk0MFd4U1VVS0JqM0RFVlFFTlVWRyttVkx6bCtQVFVjRU41N1ZYVGRDM0RmVklYV1N0WStTbTFQR0VtakVleSs1Qk1CSFpsc216dXRUckdFbVlGc0xDN0QzSHNjMHovZlEyTGkzZGlZKzd1OVljTUxCdkQvRmVhSDZvTXNEaFhLcVMvb01mY2lvQTI2a09mMGZieFg2UStGUnJYSnRodnQyaVJuNUk1dVFKenQ5NHl5R3ZtcXJtZmF2RmhTQnRaYjZXZC9qTzA1RVYrRkdTZXRQbGJuV2lSakhmcW9iT0hPemoxbWRQZDRSUEpjbHRETGdSbGlXTWsvWis1MGl3QnNiQlorWnVWZ0J4Zkh0b2hEeDFkK2s4VEo4b1ZmQnVFSDQ5bDIrNU1QM3J6a09qTzhYcTdlQmcrUlE2eE9KN1orZXBHNGgxU1FxSTFjMWtZNDV4d1JGSVN2a1M0MHNKbHZKenBiajI5UmJ2NzVFZUg4RjVnTlNLVGQxZ1loNmdJLzJCKzNhNDNKSUN2TlFoNmZSdTJ0ZlVZa3pXdW5Edi9UODJud2Q2eWtSaWEzZEtkR3BoODEyZ3d1VUtMUE04dHFoQ01yNkFKMVdkcWllR0VudmpjM2FYeEsvVmVLeW1YNUdpUnd2QW41ek1HcG40M2dPTWJpWkphUEZwbkJDOElZMlYxcElnQ1hkYkRYSVZ4MnpLUlZaZkpoRWVBQnpnZ0FRUkRRMjJISXUxd3pHZVJyOTF0aUgwc2tYdzJaakdKbE1jYS9OSG5iVTk3aEN3Q2M5d3NwS1RRTjRkNTN2dk8vMlBEMytHTTBBTVNZY1hGNzcyUWV2NGxsR2FQcURSQmUxNElRV3VlZ3A5QXJsazBSOENDMHdCR1BLNTBVUFFSWTFkVC9mSnR1RjM1bERzWFZZRS9BYzFOME40TDRTZG5uRFNrQWJNMzFHNVZBZWtPUnIvT1VBWHp0bnovdFd0Ulk5WFB3Qk9GVTN4WVQwRnRVWlN5TXFKMkFid2IyN0xXcmRrTjRJOG9CMExQUUNZN3A4dFM0OTZuNnJHSXJjT0Jad3RYZFdmUUhTRkZEeThHRW1OMEROOGRNUDV4a0tMbUl1N3FPcmU5MHVLWkszMStwOTRzRFl1UFM2QTJLMTErc24xTVFwSnNXRmxHZkluanlpTUg4WEdteUpnMXFaM240WnhxdTNKcmEra21MK2V4NVA2c05CN1p5dGQxK1R6cWMzYmRUMGRwdXM1WFNsaWZPTE8wblRYK0w3RlFJaXU4UUZTY0VqUXB5ZlRtZkM4aDdUbkVncTZHSWM0T0hVUktyS0trVGhINnJYSi9jRXN5cGwvRjFSOUVyQ3FXRVlLbVVIclVvWXVMQzRvWGtrd0M4RlFQenkwVmRPZXJ5Y2YwU1ZrSnlneDFGS2g1Q2tuYjFrRHdFUjdHT3pZb01TZmtjUy9hQ0N6YTZ3NHhsSTJZbFM1emhrREYrYnBlQ1JOVDNzSHFRVmxSY2JRY095dDZWK0ZUZ0o5UmN3VHhqeXJYMU5kNHlUbmJhMHQzZXU5ZWlOUVZsRnNzUUd0N1QvMms1bjZhSUxPeS9MQ0o3YTkrNS9MaGtqMXZHYTMveGJKLzB0SEFvNjZ5K2tEekhZRHFYWDBnV2U4a3l1VW9TRnJjOW5GVjZ6OVhzLzZndUJjdWd5c09GUHFFaTBSOUVmeTBmQlhuQ0dubTY4NEpUaUJSdGdNaDN6ZEFpeEpzeEpIQTRiQmFxVXZYckJNRC9nOWVYOEg3YldkdE9ieGdEWnZ3bFh2M0IxSGp2UHR6ZjJ0MzhBcHhJTGRlSWRZaTl0czJXbWd6OExKS0I4UXBUR0lKOGhocXArNDI3WU9JcUEzMkhzZzhWUkpKcFZXc2oyVEx5RUgzN2lmMWFKM2JGU1hqS0lyeEVtMDJNUVU1ZU0xQlRqbzFaNU5HOHBaaTc0R2FXWjUyN2lZYkFKOFB2U3FTaXdWbE52RHFFMjJRcFVwUm43eHVTcjAwRkRvY0dFY2ltKzJqTFNQT3liOWt2ZmY0ZDdsdXJ3RGhsU0NYS2ticmhYNFkyZ3lTUnlGSGUwdkhqb0xQVXhQY005c2xoOW5kS0s1RzlmVVdJSkFBZUtVQjZvY1Jmb3R5TnIvalRDOXZMMFFmaGJ2c05YQlJ3UzB3WTllM1lTQkpBZm1zRkFEclFMMnlUUVpJSEVSVVB6ZjF0Nk1QUWYrakV1UllTZDJ6SjlpSDhZWGRJTGhWRG5jb2Y4R1QrUGUzVncvQ1FxbkRXOXlUUXVEd1JZeTJCbnlVVkNkRHNkaEt3bmg3eklKOHFFYVdKTWl6ZVlIQW9Ia05MMlBMZFJZOEZqWHZCaGpBdm84Y0V6OFptRXFTSG1odnVQelh1aW9KS3dqYkxlaXZGTW40bDg4dE9TQ2NDY2FjSCtQZmFjVS9KcFhkNTcrWWxjckVuTWVjTHRpVEkyb2RaQWpiRndRdHBLKzhPL1RabU5oNFU1bVR3MEZxTklJSFZzdzM0ajFMbjBjczVEYVowWGh1eUlDN1hyS3N4ekpiMVVGWkp6dERneVVJUWNodUoxanhteE1uTmhUK0FXSzd3ZmRVRi9ucWNCRFUvWDlYSjhsVlpjZHFXQnJYV3VPN2VaOTRxbi9yYTFXYTFTNnhRNWQwb0lMRllqS01VV2lhVkdnanhJamJQSlFPZERSNmg1MS8xc0NLRjdBTjNpRGIyZ01FdFBXbUlyVXZjM0lxRTh4a2xoVXIwbzNpM0Ywd25BM2hGVDJKT3drdlhxcjdFcmcya05Hdmd4VE1UeE5pUjNDeUt1dWt3NUNwaVY5YmFKdEpwN2pDNUFLdkxYQXpWMW05TjhpVkNMOXNtc0czUzJNVEROQlFnR0N4Y3RIMDBnSmRZTEdjSklvck5RZ2dib09ZYXIwTnhNTWllSjY0TXVzQXZWTlFqOHNuenN4ZDBUck4xQ0pkTnR1YlVUc3drbUpzbTJVVTVzZGtoRlRkRjY4aFk2SUtwSVhJNjRIVTc5eEZiMm9IRFVud1o5eEtmcXFMQkRNUlV2cHRiaXFCbWZZNVg5eG5DT0dqdnhUd21jMFNkVDJsSWdjNnRuYmN4NWNaUExMbXdGUEsybEJSeVpLMGRLRUR1VVZCWFBRRWV2S0ZtNm9CNTl4V3c5UDdmQ3lKZit1ZDNyRTlUaFRGVFZMbytiL0FFVmlTU0ZCQWFzWW1pS285dlVmRmFPN1NIZFY4U1dDSnh3YVFxRFBqZGJjWnBGZjhYS1owMFJwVC8zZmZsMXpWK25uRnE0cCtIVjJqaUlMcWRhNHFXSUFDTnJqUnFDQW9YQVRIeVNuNm95bWRqWWtqWmxmNytscDQ3a3FhTFhKZjRBWTVMZnRXMDNCczRxLzIyQUJmVkdWN2w3ekpQeDVQZXZKNyt5Y2gxQ0x4bjd4K1BYUXRhS3ZrcHBGRlBtTTJWUUFUVUdBcDcyVWdFLzd4RDZrZnpnZlB5WmNSVlROZk9UakRJZVNTZWdEenhHU3hpcWxyQzBYQlRJNldzUTNvandxZFlOTEV0RUZ5SG45VG4yT3dKSVNFdkYwSk9kMEtPai9ITkgyQ0NHL01MR2FidmQ2dWoreWlaUEZCWVI0Y1U1US9ucnFlNnVzYldMbTZuc1ZBQW5uTVlLQ2FzNHF6Vm95ZnF4NEJmSTJZcndXS1ZGVm1uYm91RWEvZGVTREM5S0JaNnkvZ1l6SGxmS1prVXdzTldnQ3BlK1ZUc2VHb1NNbHU1aXc1RC9ONjZ3cXV6ZjBTckF6Ky94WTQ4NmNCcytPcHRTSnFjM3doNlF5bVo4NE1BNG53RHhpbkZKUWV6eGFOcDM0cW4vckIyYVoybUpXU0JHL1JObDlXMUdnVDc5WFpEU1Q4YkovdjVEakdiVXJvcEdyb2FNNHgzbTBOTFZtbWZtZXpiMVNvNEpKSlg1dEY2c01zQXdJOVBJQUt3eXZOUlR0dzRtZkljcWQwekdsMzFLTFp4Vm1qNVBMMWMxR2R2RXJSNWI3Ums2cWZGaU5KL3hodDQ0NHg0ZmxMZy85MlNnaVJaMFZlYlB5THY1bVVwa0hSQXJIc1FPN0dhZXFGYmZwZ2ozdjJpZGJKRkxpbldmWEdIZ29MYkdWOUVkdWVCM3pTcFVnSFpYckdZek1YRTV4VzFqR0lEZUgrRDB0UFlJZkNEZG91RVpMRTdSc0pPclJnQmltS0ZMK2ZuYndkQk9weDZ1cHN1L244TE0ybzBUUGQzSVpoNS8wbXdzRUdOaUdrNXV2REMvUlVtY05JNDFYREN5QUQvMjRISjIzN3RuUll1cndFdEloczJVWUxqNEJrV01OUmIwOW9EdUxpcW52TDBjbTFmcFgreEU0OEk3VlVBem1nYk44dWw2T001RTRxWlIrZFBuYkZUYWt4bkpYS0REUEdyV3hlRjhFM1RGTXBIZW40YVQzMGl1UVNZT0lWS0dwTjNyMldYaHZBZzVIYkFpdHhlOENZbXJCYzEvRzErRzhOMDZDNTBpNEV2eUxETzJJQ3RoQXJVYzJ1Zk1iZFc2TmNGdFo1N1FKTVQ3VENTMlZOTjYxT25aUmprQ0VHREhuajJlbEtrbFl2QmxhemJiMTJ1dXNNT01yZXkxKzlrOEJmYit4UHdzOHQ5dXkrL1VBYUhKVUhMWllWU2E3MUgxdWx5RzVNcHN0VXZ1NHZnTGk0QmYzZ2RGcEVkaWlpU28vZ3RuZ2V6SDVMVTVVY2Y5dHpxUkxaVTlqREhBeVFjOGhIUG5KM2dOcTdETGptemY0SXJSNE1IaFlrZ3FGYWY5TmlNNUFZY3N6YWdXbEE2Ty96N2ZLRmJuTlpOcEhmTHN0M2h4aG4zbG5QRFNQUG1xcWFCcURQSExaK0QwWXlPR3N3R2tFNWZqR2ZxeHM1NFpubmFMVHZrMGw0L0xpV0NwTk9JL08vdko3MVpmUWhXbXZHdUN1K1dBSlBPV0NQVGN3L0xPR25od0J6NUVoamJWdTZDT3BSa2I4ZlNYTjR3cjRUamVFOXVBRFB0UVBmdkZvM1FmQlNldS80NXVDWUNpdmVHOXhXVHdrT2lnU1ErZUJ4aTd4TlVlYWdiK3RQQWJvZ0hKUlpCNEJDenZhaUhtRXNpUVh4M0p3YzlCRCtKNXRBVlEvM0JJMWpQbEJtakd1NjI1L2N5R3VYdUtHcXl1KzRMUmJYYUl4ZS9oc3dCaVpWWWpHeWNVelV6WTJ2V0JBYlVYeThQQWxBUTdFeDdCQmRiWm9yQjlrUGxHVExQTVdXYlpSaXlWNFJYNS9uWm81eU53bFF5OWh6dEZjNFpWQjNMWnJXOUZNblFRWUhWblMzU2gvR3YvU0ZMS0x0ak1OWVQ1eWR5KzdaUkdsTGVhSXFTSlIvMm00RlBLMmhTOUVHU0h3NFcrNGl4TUlqeGtTOFNkbnZPZlRmYnJ2RjI1aVRHbEhydzg1aUtZd295LzU1THU2am1lME5McWhSdFl5VzFsc05La3NZaVZUR1Y4RTZFMnNFK2pEMEZZUkhxUWJ4U0p3ckpybHhTbUZ6R3NQdTY3dy9adHVNNnZkQWtIeUlHNUl2THRSNzFCTHBWYWtGaUxmZkJWMDdWZUkyTXpPSmI0SnFoT3pWZnd3S3FZa3VyWlh1S2pBUWMxdjYzd3VMYnZmVW8wK1lESjFCcnZrSDZEL1Z3UjhHL1lwRlkyckJ2SnlzTFpyUmJKVVVkRHBpYkxSWGVUb05aZFZxbDdKZjNBd3VZV3hXWCtSQ09Zc3BNQ0hvOU1DUkpvMGJZL3BjS0RONCtaYlVGRjdRTXVJMXhBbjhqc1BHMmhyODdYT21RY1JUcVdwNTM5MnorbVUyRmFnOVRIQzBtUDFEaFBTbnBzQzB0WngvYkNESkdWeUkxdktwKzZhV3RsS0M5NWVvNFhUMjFLb2RuZG5xbGdCbUlOWmhsWUloWHkrNEw3TThDcTRadnpJSUJuaVA0S2d2c3psRFZUWlA2RlB0VkM0cjdDOXJ5QjE4aHdEOVVDUTdqM3QzcFduWHBUWXp5NzFvZTE2cWVPWThka3hXaDg1UFpxY29lbU1Wc1NWdXdlNC9mbnd0QmZyMUFleWRHVXFUbXZtT0hoaWlUTWhmQ3QwY2kwc0dSbE1PMVZJb1BuVlRuVlhCbWxUcWhFa3psQ0REcTlaeXZidUdhVnU2bXNXM3NJc3ZoQWNWbWMxVzFlRFl3VHB6a1dxN1Bxb1huL0JvaFpZc05nMUt3NWlLTFpiWllBbWpVWldIdlEvTzYvRHNtUkdCVzBZbVlXMGNReGNEaXhKeXVBZUJHV09GRFM4QVl5NUNEWUNPLytHN0J0R0lETU1HOEltNks2NkJVTXdpTnVqUGdSWXkrOGNOYk9ZREsrN1A4ODdRMmtrbHJMMk5KVE5sOU5ZTk9Kc09rdk9DRGlub1gyZHN3bDVyL3hwKzFPT3V0VEZiVloxdEFCamNzc1NocS9TQXFpbmRkakIyUzBLRzFzeWJqRlJPc1lmakRCSFc5MzZHd1F0amthc2lTSkxnZWZwWm9vUkZnZHgrSDkwa3phZVRWWWhyWUs5YlFrSUM4djg3aW5vVWVPYXl0aUN6a25sV2crUWZYeGdua1AyNXcyODFSWlRPUGhXejBKd0tkOXRUK1RFS25MaW1lRWZzRm0rRGhLRlpJajAyaUlLVHRzWUtuTXFCbi9nN3JsVjZIQm1waEd5bVhxQ2RIZHY5UnhRcEs3QnF0NGR2SlQ2cHRiRGtuUlY4SkVicGVjdzFRZEkvY1IvQ24yWG01UzhwTmlmVFV2SW9hTjkwTit0aElFNmh0MkI1NDdTWkhnRzBxbnhsZXgrRUdrMTVOa2hYYmd0aTUxSjU5aTBwOFp3R251OFNCYzg1a3UrZ2pRVmFPanBPWmt2b2UwV2t6OGEvb0N3bUw2cGJMeFFJUUlRcms4eGtjc3hoOXNFRkJCODdiVk55QnVMcWdEWDZrbDcrWFk5RXpZZmRyRHRGUGZValhnZTFybjlleXpzRUVLOGs2Mm9LNzBaWmZmT1pyRDViaEVDMVhOdjhSZ1ltcGpyVDJJUy8zVUFHZk04MU55U3JVd2cwMzVsVk1sZ2tzUWpBNEpVL3pIMGlVZGw4YzY0UlB2SWNDblBzQlMzcHpOcXVOWUQrNzNLMSt4NzZqYWVrWUlJcHUyMjBCeThDZWhNRWhVYzBQNkN6eDI5ZnJhU3ozc1FqL0J0S3dHU003NmpYTU85VWJKTHdhbkdkb0Vha3R4aGJIRGxEOHVIU0YxN1lmZlB3TEtaYVFxcWlFazBMc3g4dHU2UEtIcjVnZzRtSVhuR0V5Wjgya2xZcVpqTlNHc1R6ZnZ2OWc3RGJNWUJjTDZGOXRmT2tkVzgxUm9ZZzh2QUdMbk5hVWowQmY2NDBZM2Vsb1BkRGlnQnZyU0VldmVmS2kzM3RKM3dPNFV6ajRoWjNaenBybXhka1NOYnpQVFU1WmlGbS90SjQ2enpFL1J6dTdmM0hJNnRsRng3VUdIMFhkdTdtR2ZaZysvZ3FGZ0V3KzBYRWhYOFBUYWpxWW15c3BlRWkzTllXZEZEMXlNV0lmbTFEQy9vYjZrN3IwaTFPUU8vZmJrT3o4ZUdhcnprdFM4UUtXdkVOSVF4OVBxMTBMVUdpcDI4bkg0aFlZcnJxZU0yc2N2enlreVNDS1Y1ejZ4MDN3WFp4K0hiYU1UY2lBSmtrT3BrV0RodW9iTnhRb2tmb1lsSjJobzlRNWFDenZmUHZORWVUQmNTOWhXWVFvZ29MV3BGMTNsSGg0QWpUME5ibWJjUWlDVlB1c2dHNmQ1bC9NMm1sZ3FiVjZOSFBQdFR6Uk5zci9JMTEvNjErUlREKzFHN0IwL1U1UHB4dUtBa1RubEhvZDVxKzF6eThibHlyQTlDMXhJTlpyRWQ3cTJTQ3dGTmRmaDRPSkllL2Y1VXJYV0ZZVnkyYmhETzVjYXN5M2EyRjlrUFQ4WVJqamxyak1XTFF1akxRMHZTSERwZlk3OG5FL25HYTR4R3RwWVVSZkkwb1pQMUZLWDdJdkUzY1VpKzkwb3p6VTI1YW9iK3UrWEQvazdLSkV3YkZHQmZVWmdDdUJkM3U1Q0FQZjd3TkRBcTNwT243U2E1ajdMTE9TNFJNUC9Mc1d6bTlBenE4OHNDR2R6RHdKZmhQbzBnbmtRTkROSGhCRTIwWVE3QzhTTUN5cTgxdkFYU3Ava2x2Q080UTZlNGtOOFJlUlVFNCtkbVh6eXg3QnoyYzRiMzFJWGFBNG80S2ltY0EvKzFWbDVrdUZFYnI2QTBWRW54aUp6ODB5bHI1NDhncFBCeWhUdzM5TEJhbGQ1K0VpTHRBUmx2azlMVlZhdUhra2JHZUVJcVRIcDVydC9rN1dWdGdwSjY2RHdnV0xjQ2dvZG10N0sxaFpwbkJlUHp2K2xmL21QU25Ib296ZDVkVEx6cU1VYkZKc2RHQkNRYThQWjVxTlIwcGxmWU54RllxbFVZN3l3aXp3YTdPYXcrT05KdGhLcjFlR1QyVGppdjU3QW5Dalg0YytFMFV4RjhNUVlMckdtNndJY3lDTW85bXY4WnJVbzNGOGoyYXEvdVJ1aFR2UEREMDBXRmVzR3NYeHduUmRqcFdHaTYycnF5WTUzb3d0S2czajlOYXVtZEJvVXdZMEsydFJqeGFvM1A4bkVKY0E2Q2tKalBxd2VOOFIrbDlGNUxrUEw2SDcrVEc0TWhvN0J6QnYyUkxTWXhsREE4OURWOHFuVEs2V29UZGpUOUJYMytTOENCZEthSTlJbnplc2NHOVg2RTBySmdkeXRMVWZnSFdxWnFoV1Flbkg1anhrNlZLVTRGcThJVitMMmVhUXdKWUlKZUpUMjZsSGtrNHdLSEZxSll5QTVyM1dPaDhKd0VQTTNzeTNrYVFoOFRkNGNtQzZXektkc2JsRjNiUzhZQUdJdjdwa05BRnpXL3hQeUg1ckkxd280aDZuVGtqaVd2Skl6aWlyenZMenducmhXN0s4azZtcmExdnhQaGgySGYzOTR4Qk1FQ2pyajVOYWxNM00xRm4vNmNVM2tiL0ZYQ2IrS1BrazEydDNGYWxTY1ZFam9LamQycFpSNXdvc3lsMVA2U3hTTUpIdXdlYXVVYVZUNVNXdnNMdXlTN0tWSjlYTC93YUdRcU5UNm8rM3RCTHNQSkFTZWFXRE5FaGtqdGVYRUV6VmFrZkhXRzZlbG5za3FKWkpQTC9XVTRickh2S3NUb0N0dFNxN0JDUW0vYXB4QWloWkxmdk1aRnYzR240OVBKQ29RM0JocU9NcDVOaWxjK1NRRjk0VE53MGZmMFZ3eXVKdzFkUWU5ak5ROUJiSFdiRHBkY3lWZnpjQUVNaVZFV1dobThrR3VFQmprUEwrNkhTVnRIZHlZZTYwTU55Vm85RS90RXdwMVdWendqRnB5cmkxRUYvaEpTQTdOSmxIOFIzOWY2cHRqS1NGVW1WOW9UbGNNL0RKd29hM2RtR2dSVHBqdnExWEtRRWdJMDBkNTZxK21ETmhwTlQrenZBVGxEaWFmOXk5ZFF4bElsTFVlL3Z4OFlBaUhzek1tL2pMSkkxVVhTeEpqN2thUk5ZdXNLaFJXVFk5czRPN1g4Rk00TE5BcDM1bElFSzg2c2cyMUtDOVlQL0w1T0FmdjEzaWkzbzNXa1BiL3JwVyt2WWl0NmsvdjNidW1GTitFeGNnbHBuT3kybCtMTHdKbHNNUHM5dnE3Mlo5VCtjMEJYYmIxOGtjS3pXbmx5SzJMVndORGQvS2ZSSlNWZm04S0w2OXlrYWRJV2RLYW1HWEh0Rmg2REQwZGpjbys4UlVjdzRKY1gzOXZVR1Y3UkpPdFpQMHVvT3RJenFTc01XZVFBcEF1TDNrN1dUNlV3MWVucktTZXFpTE9LcjRYaU5XaVV4ZFl3NVFsd2g5c0QrNDJTR3VpTTR5ZFVxQjZIbjRlb3JtQmtlaXhnRnZyU211UjdiRDBiRlNYWlJuM2Z1WEJkZStaSXRWVWFJYWhNbDdXRVowY25FQnBmUW1LSGltVDFrK21JRTU2YTZBM3d2V0NOdktpUm92MGlsVlZFUVFIOTNxeUNFOXdmNU1hVDdYQmZQQU1FZmNIWmtwY2JIT3huOXdSM0pjallyVnFSdzVRdzZxbVZBRmFaMXRIUktkYW8wRkhScTBJeGNvSktEVGtra0I3d3NJaW9HbVFOOWN5cE9zYjJMZTh3NFdxZUZpenRHd0pIOUx4eHQ0Uy8yWGxYTlVuRzR0TXVTeWVOUUwxVHpGWEZMVk5qeURLaFp1UW43ZXE2bm4yc29IMVFZRW9qcmJncFNVMHArUmN1dFJxR2llYmF0RXRSZXNOU2ZoTXdnTVk0SkExUitkMUtJUG9TaEFWNzMxaXpZVDlWRzFlNkJPYjZibEpsVVNkS2dqOURhOGlrbGNiWFMrcThKQk90SUorYWFlZkp6TXdkd3lFU3B0VkFROVBUVkFqbkFrRGs2ZXdJV3lKTWh1bCtQV2pCME96ZXdYR0NMaGlVb3ROZXY5b1o3cFRQak9wRlA4OE5SVXZsNDVvV09zTlV4bklLYWVER1lGSHhVU1BuaHJiSHVFTUM2SjZMRHkvL3VCczROS0dTY1RIZEwya3pQVjNiWlozL2dXZzZtYksxcHpqUW1JbSs2SmJBUXlXUWJlWnIvVGg4MlI2WVFQU1czR0dBbk1RRHlGWVJEdnNLNFgxbG1mTWtJd0M5bUE2cUkzUzlrd2U4YlhMZGEwQ1c3ZmNUSFJXblpNbURETzlrVERvQk9JWlplZHFDV3hIRys5Zk53U1lHNFdKdUNNTGJYU1d6NlRNTVlaM1ZoL3d4UlBBb2JISXBQOHhFOUxrcEVJVnBJNUpia2l4eEFRUWdlU1Z4dlVHM2I3em9aODZVTUdYTTczeWxGU2xBVmNNMEhkanNLWHVCbDJDWU04bGVsZ2Z1Q0c1SDRLTDRCZmlJcWY4MUNHL05VMSt2bzFkMndGRSsxTVYvbyt1cVU4TEU2Rys1cnNySzRtakZhMXo1SjJVckEvY250SDdhY1VWaE5Rek5GMFNhbjRhU2p4NzgwL05WL3V4WEtvbU9XMEFEZEZuUWUxeEJySzZlYktqVnMwOHZ2Um9jUmpHVTR2WVI0WXRMeVRra3hTaU1vbEVhaHljUVhwdTN2WFRqSkhNQldLMlliKzJ6UnNya1RQamZWWFlkZGRJamRMdUhhQzE4bnZqcUdTcFdmRDJmN1hNckRzV2VDS0JINGdqeEpuSERqRkRjdC9URTVUbEdyZ1VpelRQNXlVRTNSTWNYNm9aQjQ0Q3ZaUjVsMWxkVFlvbVZFYzVBWjJrTTZaWm5CY1c2NFc1VzZIR2Z4bTBwelBoK0FvSWZUemFINHhZYWRsdk9obGpqV01HcENvUmJnOFVjQTFYK2krSlREbjFxTm8ra2ljOUtyVmhDTE0wTk1SVjZrTGc1U24wRzY2cVNmSzQ1T2Y3ajRUdFFndGlJVjZRVWs3MFQ0dVo3RmpCWE42RUxESWlWZGJtS3FxMUNFK2cyZFhuSDJ3OGRtT3R1NVZCZW1VY0U4OEZMaVlXazRIcElqTWZNemdQcFVuTlBXVFI4OWJJOEhVdHlTU1QvekF2aDhTbkdoOEtSeWZKS3BkYy9aanVXZTYxamlkeEd6UGZjUE1PajJ2ZnNISWUzTmwvS3JXdmtLMnpBT1l2MkVDMTh5RWRaREhkL3JhSDZTczNKZkJLQmhJZU8xYlZQZ3R6U1BtSlBWU1lQSDZuMFlSUXpCYzlmSC8vZ00wK0VUdldGam1xVGo3R0tYalFEbzhQWE8yeEhpZEtiZkZrRGJBVkFJZmUzLzZZN3J4eGhrcXB4RWlBc05MUTB1RWVuUGJ6em5NSFNBaWViUmh2UXZtOUZrYThId05NT0JGZE1FdnpzN2FobURlVFJHaHF5amlFQ1ZLcHZKZXRSb3dPcjRHVWpDY1hBdFY5N1ljd2tUbHBwYVNNZElCSyt5ZytBWi9hVXNTeUNPazFqVU9TbElHeVd2dXhVSmJ6SHQrRy9RYUNQeGE4Uk9VMXVIcm5FODMwY1pEeUJEOEFFcXJtU3RWV1UybXZMcDRnVWxqWHJQcE1aTHE3TnFFQ0RWRXpLNzBVd3RHZ3g1OFNKUStrT3FXZStOTEdTOFIvb0I5cjgyaUhQczJSbUZHZ1cwdHRwZVR2c01Oc3ZBV1c4RUpkQkhuZHhwaE1LdWlEQUNwRDAwOWw4Q3JtWHhaT2YrODU4am5PVnRlclV4b0tZejFCcnJ2ckRuSlpxMjZ5VjlEYWRWdWJuTy9Jd2tiS0JFM0hmbS9TcXFwZDFFVHNWQWRvaWFuTGtMZEtGRXVNZkUzQXhOWUU0dzRHQmJpaVlTN091VWZEaUsyMmtwRmlzV0kzemlzbGxmNmNPaTJ1S0dhVEtMdldMSXV6ZW1xekU5UWdPSE53V2ZxUHVUeHRaQzdRSFJzWHA3MGJua0E2cE8zMWptQU43N25OY1B2Nk9uQ2NSMTNPL3FlSHZ2SzBZR3R2bmpPSjZvOXRHRjNaTzRLcUNTS1VTb1VIcitaY28yRE1CMkxSN0Z6aS9MTml6SlBVYmcxVzJrZTBrVXI2VllWUVE0dk5kMEExbzIxdjZUUitBZXRXWW82ZHFFdFZYNzByOXpYTGd3ajRPSEQ2QVhKcVJRem5QMzViMG5iVVFxangydjJpRUE0VStKbTRCTGJZU3Y3Y0V4c1ovZERReXB4YkVEODZFZG5tMm5rSC9nRXA5NEVnRHlVZDRxc1MzQ1ZNbDFjRENGd2wvQzJZd0RCZGVES1BOdDVJNk5sdVZYU2lsR1BIaUZTQXplMmVuTFllNUR5OFpLUlRxWHBQRGYrY3JyZ1JrUngyanpYRmdqUmVobzc1a3BJc2hZQkxzSGh2UVdPMUtTdHdxOXpXb0ErcTV1QjlqYkZOVXQ0cERSWVljaTVmdWlHUjQ3Nk5Ga1U4NXl0d3ZnVlRLd0VubDZQNC9vNVJKemNwbXhVRWVHbTBoWmp2cEYxOVArZG84aFJBSjRLYnpYQTZraWJFbnFNc1Q5QmZ3SUVTeWQ3bng0YW1ES1JwU25MSnBxQ0RuRkExV3FnQXkxeTF3bnF6ckdZc2tkVU54WDF1Wk82SkR3aURhRVgxelNUSzRicEl6WWVFeU1scFZVL29JUGplaVE0d2dZMFhIYk94b2VLMnJxdUhGOVNweE5WbFpTd0ZVMjR4cnRLQ05lVWdQZzBXVUNGdHVOK08yenZhU051VCtYQk9GNUdoR3B1TG9oVlJEVnZVRTVySmlSeTN1TnJKSDcrWEg2ckhJZ2xVSFF6ZDJyQ3Y5TmhoZ0hMMFloc1A5NVZCZ295WWkrUHlNYml6dys0MXIzWldGSUFpOGtwNGJteDZKRC9vbDlYWms5TzNkRDRaNWhQYkJaSllTQS9jMUphUWJZRnpJNWxoSkZIbWNVZEVSTHcyb3pjYTZ3K1QycTVMak05cXZjMzNUUm5oWjJsMHVyWUMrSGVpTkgwaklPeWlyTFBqTEZEK2xKc2hJWEhkQXBuOEtYWFozNVUzejNRZ2U5ejFzUlZjSXpyME9GRnlObXgrM0xrL041ek44TDRSMXRMSnJrOUVPRmczbzB5YXUrMUJ6eEFCM2JTRUwza0poemxFVkQ2ZFVKMVREL0hoaDB4OUtWZi9oQzJrUU5TRkQrdE1iTFJPUnRtanNNMWkvclRBNEI0TlVVR05PMUZZTjFXVll2aEtSVG1VNmRkUFNCZlh0OWVoRVdFK2pOb014aWhlWjZta1F0M2FzZjNRNnV6cjBPeU1Ha3NORlhNalAvVkQzNW1xM3IxSWkwMjdxRitzRWFkbUFGNDBrSDNKcGhaeFFGbU1RUklBOXgyZVRCNTFEWVpRc3VTSlF0SFpUeVphemNMNGZLbHpWaG1qbG9nRFduWUpYZFMvTzlaTjdYaHA0ZUVNNW9obmRnUmU2WUxqZGxlMlUxaUdOUEErbDdHWHU0T0VRc2lhQW96VEd2SWlOOWdlSVlvelZGVjhRRVRKaS9xVkNMNmFxZXVCWExUeDJxL1VoYXp5L2pGamw2aXg4SjZlNEJJSk9nYjUyL0R0RmFhRmJkVHk2S2UvZFAzYVF6TGhOdDBoeXRzeXhla093OG9iTFpMamRmdUdsTVdSMXp4MU5RTTMyYUc1c3dvcEdSYm9UUHhjREZoMzdZZStSZVhCRHlFZ05Nays5TUxYbEduS0ZXMVlrUWYvY0o2bXNvallUZ1R1VjB3M1kxM1JPTG43MWovWTZxY2EyQWpCNVZ5dFhrajRTV0JUZVhaWGVKaytYQkptVGFFVFlrM2FmRW5xWmNmWko3WWdVWW9JRUFWZi9VbFY4YnZDditNdVlGaTZKUGhYY0ZVbmZSZGNwMnU3ZzJ2RElVbHh0V1IyQjVkQXoyK3BhSWdkZ0xlbmtOc1dVZml4Qm84dm5hTEVqYmNLK0FSK1BPbmZXQTVMeDJGbHcrRHRRWDUyZ2NTeVAwMytZdlBJTi9kbGl2d2xBQ2hvZ1BJdFJuV0FQQnZmVW16NlZDazRTZGNwZ1FrbHc4Nkp1OFRmRW9RRFYydUtyRDNuTDFuWTRSTEtBc09yeS9zV3g4YVZuYU82TGF0aDNqZ0tDU2ZmcW9lc0Zsdm1PU05neC8wRy9sWmZNVnFhQ3VyeW41N0tYSTAwZHlrMEJQdHBZSGE5cS8wUzUraWQ1VFdqRjU2MDl0OG01YUl2RDN2SGVreE1HZkw2KysvMzZidFFNcmJDZC9hNlR6clJranc4RGtNZTBlcVpkcjErNEd6ejZ4bGJ3RVlaejRaZEpYemdqTkQ2Y0RGNFVJT3BiMFVvSVlMMitGNFlCZHJ4dXJHaHljUmNkSnUwS2xOOWFzNm5CN1RQeUVTYkphUy9XRldaSjhWQUd0c3hvNVMza0p2WUp5M2NxT0YrOC84WStIZHlsbTFzaVBIQXdPcUJ5VXVHcjdhdldNdURxZnU2OTAremhvcDh3SGlLM3U4d0pXZ0U0Q0dpUmd0YjREcEZtTE4xaEJHUzRmM09HMG12YTliRHBGVWZ2UFNLVVZObXdqQzJicUxhVU51eWZFRXBkUnI2WTdaZERFazl5d3JKbWV4MnBRQUFFUmphUTNCTk5uUmJ0Qi9BQzdmUk9yeGtxQ0xBUTNMZ3pMOXhHMkJOUG90ci9sbmoweFNkcFdxVlhHQWczNGdJeWJYdWRmM0FDZFNGdmhWbm43R0FrQWgzME9uemltOXRNUjNnVXF2STFjd0o5bDFpQ0NLbWlPN1RQNWRlcWptaDBRNWxQMi9vVlJPU0Nha0ZObGMrdy9PSlNWRDhxb3hDOHptL3lSeW55VjRmd1dFQXRSUXBVYm0yZDE0ZUNiZ0ZtbUhHOXo0dGFTRDNDT0NiVE9ibVlQbTc3VkpEU0hwbVhKRkxkL1dBbFhhczNmY2VrWTRTZTdCV2NKbmRGTDA0NzJvMFVLUGNGaUtZc3ZSMDlxT2tjRHlGWGVuc1NjcWFoOEd1MVNnMCt6eGRsNnRnUFRNWlZ5b0V5QzJjWGQ4NHg4WndCWlFFUU83bjRWU0RQQ0pmaFNrMXE1R1A0VW1OdjFoU3h2Sjlydlpma3lxK3grZnlEcUFRUmNVRDNMeGUyQXVYK0twM0NKOEd0SWVkQkYxR0JMM1NXV215cEJjTUJmcmREQmVaajlZWFIyQjUwUkVUUWNxTVUzRHFteXZ2N1MyQmhNSVRBazFvejl4UzdnbjY3Q1AzWDgzc1IrdlRBTzdOVkYyaG4wSXFISzdtWUJ4RFBQZXpmVHl1eEFnbXM4SnVQVEJja3lOTjM3ZSs5Vkt5Kzl4MElCSm5odm96K3o0WXFnK0xWUXdJQXVzSlRoeUQyNXRhMVZwdGE2eVloZ1ZKZ3pOUk52OGZFVWkxZUNmVEQ4NXA1ZEEybWhOUm5zQnFWYS9Bd2VPSU9YUm9LRHEwVTJITHlHMTR1Q3N2WWNTNFFtVENrM2I0bEVYbEs3OCtQeGllcEp4V0svN1p0MTlNMWZpZ2w3WjNyMDhnL05DLzFGUWUvTGhPN2J2V3BZaU00QjdmZHp4ekxxVDhiREFNeGpUOWVWQ01rTFd0bzVSb2lTZjd5akxZUzRzSUJ2MDE1S3I4dnNIeUcxaVJZeXVqNWZQOCtBTjRGa0R3SFMzeHkrcDc3S2l3YzY3UFFHcDlqOG1TR0Q2a0UvNS93T3BFb3IxMGl4N2YrbUxleFg1VjR1dFhCNW9OVHZ3NWVHNEpkVlYzSXpNUit1MTFONEd3c1I0Y05SYWFDSHljR0I0bXMyZTdpV1RGN3FzSVVSaXJDRGdmYjVVZFN5SUlrelRFWUpGUmpvOHBhWVRhNHVzV0JhTS9Gc1JsR1VIYW1Bb1dMOVFIQlBCNkg2VGsxTW5BWTJUakE2ak1MeFFUcXkyU28wVkRPcnYxRXUrV1krZTZSZDVJcUhJbm50Z3RRNVM3aFZXcnBOSHpDTjQ4NFBzWUtjNGJzZUYwV1JYRUQvWmdkL2ZkQTFPZHNtS0VsWDI0akg4ZW0wTkg2K05hVGVXWXdlSkJPN29wQThZNWZxT0Mzem5vRVFtakFiKzQrQUNkWjFQZmNFZFV3RHdPajVJb1RJVVJJZHlHc05BNm1CZ0NLZUdaWUZKdW1MWHNkeU1acHViMllnNWlzdG1FM0RMTTl4OWFBUm92eXNwVUhRNDBhQVpObjB2ZnVpZjZGcWRFdE5nKzdxbnYzcVhIRDd5ZDR2V1NIMGt4bXEwMmk5Y1lmTVpWUC9Ib3cvaURiNWJJd3ZDcENWd3ZXWUdxTWRNV3dIYlBaNXYzcWNOdUh2NWRzQ0VLYTIwUGN1enA5WGcweEJURWVhMFRKYmQ2WjByb2RJa0U2WlVFQTZNcG5ObUFmb25ZZWx6NUpIS3B2ZW9rME5JaCtTemZ6VmtkVHIraEREa3A5TS9RVFkyOURhMGxZeHRDVnByWTVjZUhROTZaWnpEalhWU2ErYkE3N25mYjFjTzlwUkZGc0xmRWZhY256R3hzYW56WW5aMjVONnZ1Sytja2I2RW9nNkFXWHhRR3BNclVDYXQ5WVRVMnFEaHcyRlZmem13OXhNRld6NC8zMW01Wm5DcFc0QzdvWTNtWUxRVytDenMwWW92Z050UDVXWi9sTmdOWDBSQlRkYUkyS0h0T1Q0NjJ3bGNHakJmU0o0MndlOWhZRjRkZnV5S2k5ZWxsWGQ3YlFES0Y3djFoMlFLV0p3c1JJM2lZNmNaVEswcHkySkNvZFB6dWV4ZnAxamxQdlo2UnBqeE85dTVNWG9VK3I0Tmw2dmxWVWlLNHRXMThWL0wybUozb2RaK0NMUmZJQ1FwcUxlTVhYdldCL1djRFpGOHIrNGNtYVZ1RktSckh4dGxBdEpLMm04aXgrSG9tWTdzajl0WUtDM0VCSTkvYWhFeUNsdW1YT2xmbFlvL1o2ZmRWQkw1YkozcmZXUlo2dU5uTkxJa0Uzd2lnUGI0TGpRKzlkRktQS21oNjNhNkM3RVFQNTVQbWt5ZDRZVzE0VjBFZmNCckYyZ2RBZGtua0NuUmZzdjRHRE84Skcwcy9xNW9hRjMvWGlMRlJnM0tGVlkwUEdWSjFkOUE2cFJ5b3pGbUkwc2x1MnJKS1g0Z2xkM1NmU1RpWnp5MCt2QURsdkxTdTVzcTVXeXBFT1RiZFNBZ25YZ28wdlpEMDdURFdMR0FpTHJ6WjJlbUJ5NGpkNTFsR2Q2UG5mVXlNNDlSNWRpSGFyZEZOR2xvcElOcDhIREtFdXpDVmw4YnRWdzc4bTh4WUFxRy84aFl3eXJoaThmUVJRNTYxU3czSGJmRlZXdWQrY0UycWI4ODVSY2JYaHRSaGUva2wzMWF6TkMrdDhmL0k5WHJHdnIvd1Q0dlJXRmR2V1Q4a3pyQ2JudmROQjE1K1RDRm1FK0VCY2cwRm02VmltTDI3QnRZM0NnalFoajBDQ0d6YWkyckJiUXRQNkUxVW55UGpBWHlCLzVJRFJNUjVKRVhvTXNDazFoODYzK3BHeXRWREJaMThrRjdwTEZuOEhNUVAybUIrejY1NHF6ZmhUN0d6dlBTWWdsSWk2WkpaYkVXVGlmVWhRK09BZWVjS3FjdE5pTzV6TGJCRFQrU3lWMVRFMzlDYmlQb0c5eEhhcURUdjJVc21QYXYyUlVHbmxMSzlDVXBKcldTRjl4L0VFQWRiMXZIb1p5UWFxcHlIMGMxYjhiV0t6dkxBUG9vcEIySThLL3RLYUl3bGNZRC9kKzQ0amx3Q3lBVytPOEd1S3huSGR6a2g3QUZId3ZTdzFyMGM1anZRZkxZdnhscXpOSi9ab0lwQ3Bkekw3a0NlM1RzVEhycXN4Vld0aFV1aUhzZHJXT2g0TWllU2hQNVpjUUxBcDJBVldHd0hxRk9pQ3ZIYm1xZFkrUGxuMFZ5OWJFV1Z3RW1LNW52ZjI0NzhuMndSRWpmZHdDRVMrcXJabjJZa2NaUW1OY1lid21reWtISDFKMXhoNERmb2hvcXRscTZNbjVwclYrRytpdElxTmluMG1yVy81ZUJQMHprYS9UOE1DVmVKa2tUZ2FhcGdnY0prZUdqa3YwQVJjWnZIajA3cHpmVjlIYkxSRER1dGxnQkhOSXpkQVUwMGs2bU43Y1ViWDdOTHY4b2t6eis0a09BRDZDcEZZczBvYnpiTkZ4dzFCei9XTm5TVjJ0dmtGYXlXTHFZR3drMkNySWcybHFxUXBEdDI5eUttWUk4MDg4UTFGZ1JTaEVDejJSOU8zeFE5SjZ5dmJLc2JacjBWN1ZQNTZsekNFUUtYQ1MyR2pqWjRtRFRONnJPTDI1ekVwY0hxZUhtejVQbkVBbGRvZyt2Wk9MSGthU3dsMDM0TGVPWUQ4RVRVTm9BcW1CQjAvVFQ0ZE94dVh4RDdlUy82V3Ixd2xiVTRxRUh0dS9yYUVUNllJN0ZTNlNCaWtKcEhVVHFscnB3K0w1K3VTSTdpcTNaelJmd1BGKzlzNEE0Z2lZRVBzUU43VVVIRk9Od2p2TWxuem9ZZGc3VGlWWWZhQVpqMys5dTlETDdFTXB2RjdvdHdRVmhtZlJuVmVVei9Jb3Nva2grNThOby9zRjhRU0srbEdkZWRDZkxpZ0pYeGRDYVR6L3A2ME52cUhLU3B1NzdXOHdvbC9ZOUIwTU55d3lSeFlvdmlMS1lZRkhBUktsWE9KdlFaOGVwVXhVdFo2Y0VPVVJYeVAxYmlQSUJRUWkvV1BGeHplV211aERXMFA5cEkxazhnZHBFWW9DQ1gzWXYweFRlZHBqTFBqZFRxQ0hIelVRdVkvaEpkNTErK0FlTDRqRmZLUTVlS1ZjRWt3UnIvZi9OSGFKaW5OUWlQaG9rTFE3UzBzL0JsOHNXMDRoTGE4eGt4VnR4TW82cWVYUk11bWhmamVDYzMyV2d3OXBVcFpFQkhsb3JWRDdMY2RVTjE1dXQ5OWdIZWF4eTNXdmFMbkJrOXdkMjM0MHR4VWpOdURCUGowaDhnYVpqQXVjaS95UXRxV3hWZ0ltSFoxcldrdXpSQURobVJMY1ZnWXRJamMxL05rMGMwNXEyMXFZSzRISHNEeUEwV0RueW5nME9oWkxMNUYrTWVpdkhHa2JldG84dFFqWEZtTGRaNFBJMkUwaS9ET09ramY2b0dscUd4WWZydXV5MTdKL1RTYlNlQS9rK0RoMjM1QkJGRGJLV2FmSktDc1cyeUVUWFFPN3R6L0dhYnJtTGw5R3B3ZmlPeXE5MU1ONlBrL0RqU3JibWJLKzR4Q0VlRCtpcjhEY0RhSWN0U3pkeGJGNzFaaTAzdXVPWlRGN0xBcWpST0lUYU5mL3BWOVQ2SzNxUmlDd2lrVDhoWWpWRm9rZzVNOXZ3eXVhcXErK1dpK29NQXV0UXFnQTU5TmRhSWVZeFBDd3g4OXFXaDJCeXpLdk5hMy9aWHVXK0tuRkFRVlZIbGZxY0pwcElqRGx5UjBXQWhUZ3U0RjRjMDJjMk84b0NscC9VOSs0YTNUdDM5ang4Zm14dWxlaGpiYTAyL0d4WVJoZm1LRWhWQ2VLRHV3R0IyRkU2WmhFZERzbFFrSlg5ejZ0cWpWUUp6R0pJQ0k5bTN4eE8zMGJqNEk4NHR2NmJwUi9GaWpRSkhnWXR6Tk0rQzVsUi9tL29ESVRsZXRrTkd0R08wK0o1N3dLTnRURUd2S21la2l0aGhqMmJ4Q25rd01zMzhTbHJOWG9FSlNEOXFicUI5cGpvQWdQcHY4aWJEaWl2K0x4S3I0c1VZM2JQZ21VYlBXZDlmRFBFdzhhQTlUZForeXJyNUlwSFVpbkpTeHBydUp0dmJObktpVlNTOEtmeEVhSlF4ZXNMVDlxeTF2cGZqRGdzWHJrd0lMc0tkbGZRRUliSHNLMUlCS1prSmJpTitsb2Y2RFp2TmloMzc2RnpkOXNxdStiQ3poTFBKTE1EdzlnRVVTODJCYzVsLzNWVFN1YUQxcm9uU05OdDlLaUlvUmFKMXFZRXVoWEkrNm1XUCtIMnZIZ0pJY1ZleGJzV2tNTmo3TEpnZ21CZTQxVXhZcG5ucW5sanJLaGVzZnFrcTNNZlBVUkpUWi9lVnJ5T08rMzRLWmY4eG16aVJBNlRxODU1Nk9mblN4c1dxQkFrRklNUEtaR08wWDBFaHkrMi9LZmQxOUNnQzhuME43c3ZxN3E2VXJYU21RVVl3dGtiS3prVy9tbkIyYVRvRktIWS9DWUdWa1RGTnYyVzJVRGNhVkJsVDExcGo0dUlWQ3RPTTc1WHFReWlOSWJySHZLdjlUUjBpRmRNenQyMmxoWlkxY0swSXYxaitQK3JxaVl6WnR4ZWoxRlV3Y0tNU3FpOG5LdW5SQnkxaFFUejdad2pFN1llSXJhRGIybFhBQ3JwN0JXS0ZsNWpYQmlmZmVzY29YZmx0dGorQ3Y5T0NzUkNaVlhwcWExK0lZbG5FT0Y3Vnh3NzFpWFpjQzFEUXFrV0YxdjRsdlFFK1cwbm5hWVJEV2hhdzN3K2UxZlJqUmNveU9FWVBHVUY2YjVBaWtLWVlmUjFHdEFHai9wQ3hkd1Fjck83WVVkcDRyOXVhTlBWTmJLaFIvNlRZUHVaeVo5VkQvaFJaTmN1SllicWhBYTJzOWdMdXFNZkdrTWU2SWFOTjIyS3lNRENEQXdkSVdOaitna2JhTFZvMzRITlFuVkYwcGh4Q2tKOFB5M200QVM0MXY4TCtOMEFSNllzSmxtTVZGVG9KSDBuR2k2N3h6TW8zV3FHbXRYWjV0MmxjLy9paFNENnpMRE5FTmhndjY4YWRQcW4vQnpmNnRsckdZLy9nWkpFcm5nK0Y1akFZVVU2OUlUazJVdlg4MTUvTGlFdU80MFBCUmFxcWtSL3hMRGdXZmZGK0grN3F0cndBWVovQmt4cWpFbmcxWHdhVVBGZDFLUWtXdGYycS84UjZoZGRVMS9ZWmdIQS82am15OTFEUWdGWmVJRWRPZmc0SHd5bDBNZlFhYXdLVXhiOHNWWnBUZTRYakNSS2gzeGo5YythNy9makl1dTIxR0JrUENxVmJWMy9ocG8rUzZxLzZ4cnQ2bldqMGpRcGRvbUVGM21leHYwMEZ1MFdGNUFuY2ZzZDdQdExEV2V5L3hzcmttSkNTUm5Kb1F5d2N5elFuNDJ6anJSSnJmdXh0bGZoTG9EUXV5ejZSSzcvakIzM3Z2TkNjbGdWa3BSRzNxd0xMRy9ld1ZQVlMrVjRlMWJFYW9FOWlRNVh2NU9OYklkYnhPaXJnNzdxRU10dlUxQk05emRGUnMrbkpKdzJqdG1SQjlhdSt1Z2pHRmlNM3VZS1JrRU0zSm5Ya0J6V25sSjV0VXlYOTFSQmxLOHUvc0xyTmlKeEZvZGhMaVlKaU1hTUFCTkcwckxLR3k5Tkhmby9vWVc5UllkcldJL001bTNBdkg3WG5TdUJjVXVOR3h6cG8yVWhqR2pGY01kNXdyNzFWTUlLUzExM3p1SnZHSHM1T1pra2wyYm9lQkNzNXlrVXQ4S3ZYaE5lL2Z1ZDhMN28yR3JwMkZ5d0Zidnk2ckx3RmtDNDRTK0FSV3dBN0pWK05EbzJrb3FCVUx3andRRWZRSVBacWExYXY0cDQzMnE3aXZIQVFZR2FqUEhEYUM1MHdCOTFGWDdteFVVR3hBMW9Ob0ZDZ3hRbmFCejNhT0huMXJkY2tSMTR4YVpPbWk2STUrOTNBdkpldEFMTnNMeEhaT3drcWpkZW1Pd013WHhYdVJMUmJ6UTlMZmpOVmUxdDZnc3liSkRuTnFUM1hidTVxdlZHT2c1cGtIeS9DWmowY1ZlQ3dVcHFMeGhRUUIrVjB4cFJOeERBUXMrR2pPWWR0SVZnMmpVYWphdmJIM1c4Z3RqSUJiRnZuamtqK0N3ZFZIMldUblVmSUUvZk93SlJlZFhiQTkxRlBlUCt4YVdic3l3UmlaNUpLaHBqMWNWN29nOHM3VlhZc2NjUUNxQXRCUVI3QnkrYUNOOFlEYlpKNGMrQjVXRmNKWmJnVzA1NWFwSkh4dTZvQWt3MWV4V0RFdkVVRFRSQ253Y2tvRzg5Y0ExNW41bzJGSTVNS2Z6RzQvc1pERXVGZEJxdVcxZUZvdHJVY0FycU5FRC95TmtJSVYyMmhzcm9oeWR1ZllMOStBSjVzSW8wTHBLd3hWZlVTVkc4SHU2Wmk2eE1vVjR5UmtxNWlNUDFEcFkyRXFNb2NuTUI2ZVQ4VkJISkRHc3hCSkRZYUdBQXdFVElyeWJXY2xSdWxjSmRJOEhzQ0VhYkRmaUw2NHl2bjRRcGlYTjhnb2N6MUhZVk9DN0tYeWhpLzdrZ2RPSithbHV4dXROOGU5K2NwNlJIWUg0b1lxRGpZNHZCUlZwS0lsZUlxVU9OOGxaOUlFZy9oUFBibGdyUXU0WlVWVzRYa1JpYUNXZllYZnJYUDFjK3UyVE9GY2FJcVhUYmUzaFA1MTF1ZEhoZFNmUDljSnFJV01FVjVocW1xdzIrT1dwTjlpWlIyck5FNC9sNkVFcklRTzFRU0lwMmU0U01MTXlROTNDaktXUDFtYlltcUVzSDNqdThGNGxTUWg5Q2V3WFl0RXFsYlRZUXVIV3MrcHdOZkx4TXFHNXNBbFhneTRDZ3JrVTVmUDAxYlptN0NXYWNaMkoxMDM3SG1LOEIveXp5R1JHUHZ6dzRuWXVBQWpRTTZlUHFKMjkzZ3VLV2ZLTUtTSlVxYmVZQWZhMmlndU9henFUMkhOT2Q0Tkh6WEl5NStoYms2YnNwdURVQUU3RWJ6WFZDSjAvTXlFSUgxRER6L3FJcEpjek96T0JoN2FCMEgveDdYWjVIZjdkMUdVK3ZpOEU4Z0dFc2RKNkRqVVdiSmJPb2k3YmVsU21FTTY0S2s2cEpSYXFDQzFZZmF6bHkvQ20xa3hVblB1RDNFQTE0bkF2MXdkcEJXcmlnRHVhQ015T1I0a3p3UEhsQU9qUk96MVcxUzhnN00rZTM2NXVwSlJ0bmVDZUJaSlVYOHdyNUpqbFJhSEU3WS92bmduOTRERTRwQVRDb3VkL3EyN3kwK1Bqb0tDb29kaUhlSTlKLy9SaDROZXJqSWdZVDNKUWRXSEV3VjlncU1HN1FuMU1QTFFQb1FRTWJocEk5YjBrUlg2UWtKYTkyeHVqN28xNFdUc3ZTcnRpZXM4MXZPU2RneFlyM1J2MFNVMXhJajJrTk4xeWdtbUQxU1VxM0RHNkUxR29Od3BMbGRtU0JqL3N3MHU5ZDg5WHdoTi9TZDBDbXB2MjM1WXBTS0ljUGVoTmVlS0RyOEtuSTB2WFhwKzZyZ2hna1JraThIek9mZWlVdHFWL0J3c3RTUzcxSTExVForV055d1JrazVrYVNkTXhrenYxMm02QW5wUE1SWFMwU1RWamJMMUtockdYdHNSYnVQMDRKeTFhUlFjR1g1T3BZU0tRNW1aQVFEaHRGb1h0QTlpZUtpZkxTT3d1N3d2emhiWEp6MWhZZE9ISHB6WmtZRjZOUzdwMkhyaWJnTDlqN2VVK0R5LzdocmRud0s3blhUU2RjZWlVbnB2SXZuc3lFYkJyVWRySEpqZU1HQkxCM1h6UnoxblZUUmtUVUx6TXRSMS96bFlSVDliOTVUdGFJbkpsbGlDSkpXeHVKZjZMakdLcDJpcUlRdXczNkl4MnZlY0JHWGdobUZ3RzRXWnZSRFgwZzZsamM0ZzZLZ3JpK2FsRXhqSDBBWUpmeStMenUxeERQYXRROHdvdmZYQ2ErcXVUVno0aCtYTTY4dWpiZU5sMUdPZjF0SFlxcDdkM0FUMitiS1hQL3VJcERZcE9GM0o1NmpJYzY1eS9nem51MHJ4V2RYQlg4Q0VlaS9aK2EwRVo2RHVWc1lJUzFWZWR1aHlqWlpSeWNpdmRnWXAvVmJNcSt0eWdPUFc0ODdPWkJpYi8waFJwTHdmV0QxclFjQUV6OERNUGZLN2FCYmYxWklGdDZTU0J1MDhERDIyTnVmcDBEMDRMTk94eW1IdGZTYkxFbk16eENiZk9xTm5jU0YvYnFqb200eThhZGJNRmtvR3gvRWNUdlM4MEw5akRNT1FsMDJwVnVKc2RudndZbjBXNmg4eUNxZy93V1h3L3FMS3lTQWliTVRBVnVkZlFnd04wTUZmUjE0QjBjZEY5aGd0MWlaNHpiOXZRSzcrUFQ0RWNZcS9URUpTT2hLQndnVmVpWXBqNGZhNS9UZkRWNnhCNzllSUdZajVaT252K3ZmbDg5REs3dXl3QnMyUHgzRG0zbWc5SnNXbXpLYk9Ed1JvbmcrVkVaT210SjExL0tSR3NUQlVpMEdDSW53Yi91bDhMRklKN0NpQm15Nytwa3FRWFU2ZStjMzhsZ0ZVWldSSEx0THY0VjNQU1BvKy8yOHQ3NVZid2t2ZW9ndzhFWFB5OU9ZcVlOZjJLOHBWcTNscTg4K3RXeVUxVG44RzAwY1RHZ1hLbm5aTzE0YzRwWCt1WnlUdno2UkhnNldSSlFmZkc4WDAyK25kZy94VXVRQ1Z1RnBDSFJVTnhoYUJwVU1TRHphZEFOTjZtb1pFUitNSnhMdXNrUjdoNndyWUJMRG9PMkhOWTE5emYyeWFKc1h5UnZJUEV6S3hLWjNZalRqclJWS2RkNEdTVVNDTFNRQkpLU3grangzNFg5Z1FTak9ZdGNCTzJmUWpaVEl5MC9tR2Y1UTJpYzVFUWJqZUdoUFRXQXNtNHNPZExXMFdTRW92M3lkK24wUERrKzJpR0JDWndlNi95UHNEcUx2M0liK3cwSXpWdmtDeUR1dUYwNkorYVl4ZUl0UlpGVDhCQkhNeVhiUzA0cTJoSktQMXhGYzI2ME5OOEtSK05aM2lOWUMwK3Y4VmxFa1RnQy9GMVJQS1hwZmVkTjJoUHRza3A4Q3lDOXpNWnIybm53a1Jwb1laYVhFU3QrSnFFbENlV3Q2QjJ6YnlIUFduSGsyK1pPUHJwald0bldsTWIxN0tTL3M1SWQzbjBSSFAzTlo1Q05sc0RmNFl1eEZXRFB1UU43SlZaNCt5NGJzL1dETWt6NXNmT3Z1K05oeHBsd1NmdVRFR3BPYitxNzZTQ1ZENW4zMHh6c0FueWRoSU4vYThBT1VyNWZ1N2FHNG52ZThLNDVNUVRYd294UTZuUDIvV01RMFhEZEhNQlF1aWkyWDI2RXZNZi8wZ3FoelFFZXdYeUhLT0hTa3AvcjRpUEY3bXpTdytPbkJqQm9CVWZhQTVoR1BFYXRIWUx2Mjk4M0FneFllbjhjdVVLRDk3TGlGSHBLTkxVYzRXSThyYkxndEtVRGkrdTVNTjlGVXlUbG9zb3RVcFFRNWZaQzB4VWEvWlR2bFFXejlKcDRJL3lHUWFmTEFuK1hDcjJzUUpqTnZpR2lsSE85Z2FvdnZsK1pjVzJjb3ovdWpXakxJVU80YytkWmxOR3NlWFZmY2xVV1p6ejdnd1dVaEJzYzEvYlpzdzcrZ2JrUDBaSEdzQm1qWDFENThvbkNLYWZMKzlPUzZSQWl1RFlsTkV0Qk4yTkY4VzlLYVlUVjJ4KytydEZEVm1telJFMXBBWnF1Y2EvNGpNRnJ6blA4VU9FeElJYTlndGJJNUVkcVYrWUJBMXh4RThIT3dyaXpUeU94WThGQ242YUFoWlR2ekh4cTMwZndybExxcVJZeWphSnpPQWd0aGMxckhXTWhTUmNpTUFiTld6RmJncGJBMXR2OGtVSmY3b25RZmxMZlJ3WGRmQUI1SDY2S1lmdlRNNCtzYUpvZVErOTFVaVUxOWFCMFF4NVRaUExOWEtUZ0FaR2xyRHBLZUdFZHBBSjF0L2tIcFE5b3QwNkpFQWxNR1ZWQkFXWTNVeElLbzFLWDhOLzY4aGkzcnVNRExNK0pKY0Y2dmcwSkJxMnNoNDZDL2VLZVpXNGRPMG0xaUwyZnV0WmhLRDIwc2hFWUtOd3RWcjJjbTAzckkvNVcvbjFQNDFzQUlLVTQxUkZhc3QyTjM1VVdWUnhUWFNKellqQVQ0RGwxNmlPNjVWajA0TjlwQmoyclBFU3RuY25XUGV3Y0t6dXRFaVl2UGhZTzJRc1hFcnNrWnBjZHpzOUFYSmZheUZYeVAyRGRXd2hQTkFKbDJOL3Y2bU1ON2RVeU1MYU41NUFKSHVDRTFnVkFCTk1GSlBScW1ldFk0ZXJ5MG5EZ1pOZkVJN0p4UUJpNnhVb1FzL1M0RlpSV2NqVWU1VHorc2c2K3g4SUc5bkY4bHFWYUN5MmhsSTFmcFVValhsUEl3TXJsa2d0dDd1OXkrZEtEYlZDYTd4ZEhhbktXenJLa3RlS0lGSUx0ZUdTNHdTOW9XTDBHNWUvWXRVUGdyc0NQNWgvQ3RUc1FKb0FtUitlVzJqemFvYzU1cHdML0NlUHpqcE1oTFdzMlhyOE9rTjdneVpWcmxnc0JDaVZSY0poRnIrY0ZoUytPMlVhMWR2Y0tIUkRkdGVnVDdWem5GRnpzVjFjdXFMdW9TbzBwd2ZONmZzMjJ4djIzVktaRklYNURxbEZhR0JiRlllL3hQSlE4OXNhbWtJNnltVGtVTEVqVytpbG83Yk56YmM4a1ovcDhWS2J3S0N3WE04Y2s1anhVSWpHSEtLYlJkZ3N4NkZ2UDB2NWlmQmVBa1NwK0JROHdEOGMwVFZoRGlqTXVZVnZhZlVyNmMvOTZkSVF4MEhZL3MyKzNyR1REcTlvMHJUQi9Gd2E2anhiN0ZVZEt3S0VMSnJydTBhdVh4VnhOaldNSHpPdGVWdmJ6Y3RtZmNyWUtFeUlPdjk2NDlNZ2NjOXhYN3JIczJpbnJuc3hlcUI1bktRRWFQVUdjbFZRck5WbWFGcklXNWRyaGREZDBKNHdWOGZseVVQcmY3U2dHREhSOHNaZEFKKzl5ZE9pR1RWQVNrWkZUUm0rVUFXZFJReFR1UjZUT3dsSVJ4ME51SHkzYXRteHRnejRDMm5wZ0lDVjZSbnBvV0llRDhwM3JWZDFwd29CS0Eyb05rNUthREVlajd5SHhvcUdCeWdQOWNram1XRjhKMHNrN244Skt5RDFWRmxxbTBHejMyWFV5Qm5ySWdYYW43ZmpNNEF5dXNEL1Y1ZmQweG80ZENxd21Ua0lKbWlmNWx0bWVXSm1JTXhXaEw5WCs4R295Ykp3aE9tQ0ZLQVg0OTdpdFp4MEh3RGVuOWlXQVBqc3UraVVtSllIN2lSUFdObWpuNURyVXBMakdyMlBpb3ZOUm1tUzRLNzFVeUx6WXlmdm1aaCtmMXp1YWNINnJsZTFLSUtuNVVVOHdzellGQUN0SGoxUGU2MW5pNnl1L0p1eVh5Q0NlVzJvTm1wcDk4dmpSRDQ5UVM1aHpmVWJxdzVRb0NFMXJVQmkxS1VPdlNkenZsRUJCV0dZT1JWSnhuUDlDWXQ0c0tqK2lwSlYrbXNpdjZ2Tm4wOHZaRFMzWFV3TFVxd3VoVlpEYzF4b0Z1cjNXTzVkdnVWTXpZaURIdnI5WS9CUWxsb2VHZkdwSHo2bWNaUUUyNnI3V0l0WVhibEhaZUM2ZjRDNE1QZ3VZalYvTkF0Wm5adms3UkhLSnhyMWtnNnZrM1JMcVhYUTBTZkdibXJiMEFoNHZEUnEybzFXSVM3UmlYSXdqK3lTQUFRN3JGUVBYY1o3c3NRSXRkeWVvSWR1M2JDcjk4TXpMN0NFcXI5T1BXRmtUTDRjMlJQc0d5M01SOHhJclRjUEdzS3NOR1hCVUtVZ09mSU9UelgxSi95b2pOcHk0VjJIQ1JLWnkyQ0N3aWNCN3BIdWxYU0FEZFZudVN3anNXVDVGcUM1TGdxa2luZmhxMnBUTWRnVDVZeG1sQUJ6SmJkaWdscXhWanhqYzdHTVdkN0dtUGJlZ0lZSGZMK2l2SjhNMHJDbXAwZ2RMK05wUE85NnNRNEJET1NjU2NuSHlKUmVCUmRRcmFLUTl6L2Q4RzM3OStsYkhzZWtPMGs2MGZ2N1U2OXpQVm1jaFhwMFk0VFFxaVRLVTlDRzh5T245ZitDZ1RGenlMMGdWQlptQVVzMmxudzRjb0dWeUFTbTNrcmZTcEQzN3dyTlNjWW9EaVZYRUpJaEYvVkJOMEg1Zy9WT1Y3eTV6a1RwU1R3dEUwaURHS2o1aGlXZnVYdW05Njh3SmpCblppajFmVXhra1dJQ1ZWSTZvY1hNRlRiVlpYYVJHdCtoMW9EdjFtRzRVREhJMWRzYXFCbG9XWjZOVTVQSU1SbEM2bE5EVU5Wcit5YnhvaTlEbFdVZDk1blVEdmd0NlpDZjBQV0hoRlc0VjVWaGRoQVpYK0Y4T0JITzU5QXlQVVlRTmdZUUEyb1IyeEF5NHdsclEzanM5TnNNWjBabmNkbldQWktPRlI3OGpNUGg2aEdreFR2TGltU3VRQVBwaWhaRnR2SG5YUVUvMHpYWlV2RzFVV1lHR1JnVzFiaXQ4RStSVVZWcDZ2QnFwOGpRZFhibnZKSk8rKzByV0prTFpJVVIrQkJnUDVZUFNnL29JSUsySlQ2bmVvMkducHZmbWFLR2c0RXJySWpyKzU3dGlxQ2FnVk8vNkJqOEx0QjVuNW1xVnp4L0U5SStzbm1weFdaVGlYUkVqViswWmwwdE03OEtZZFZGY1MzQ3A4VjU1NUpQMGtlSmxqeEJOaWtjZU5LSk84YTdwaXBCdldTM2dSb2ZvMkJMVkNycGJuVHk4YVprRHZHa1UxdzFxRlVTTW5DU25nUjR6Z1ZFQnljbU5Mei8ybTV5RDRndnNjbG5Jb2JaMmhSZVFRdm44SGJaTXNzVGh1ZEZNWGVWUEIxZ1pGUGZmZkZGRER3akgyS3R0aVJEM0JUNk9zb0w1WXlEdVg1Q1prVm91aVpFd3hlbXRsa1RNajZHblRxck9xQUp6Nm1sRXZRazl3OUxYTWJmQzZnZndKT3BGclZOSVM5L1E1WENDVWRVRU5IcmhmZU9yeTZDYW9aaVJvbzJoSVNlMHc0RVN3ZUMrZlQ1QUQydnUxci90RnhJNW5GTEx4cTVnZVFRK0JUVitWSUtZTnBkK0xWZW5rN2lkTGNJbmxldWFoQ2kreTF0NWtBYXk1WnNWa3hyWU5sUitlMTFvdEtUbU5EajdqVDZMMjFFL0JMN2tueTBTeGNWM01mWEJybWpIWU0yRGk1TWF4cWVyNW5tZE5RVnF2a0ZCeUxXdU5Yb0NRRlQxK1NzN1JjWmc0a1Uwb1J4WkNhY1lYODNLSHAwdFRxTmlaR2hpVHVuTGl5UndNQjVsRHBDSHN1WlcxVHNBZHRlM29oOXFvT3F4MWgxMzBlMXg1WDRSUHRwb0lGS0d5Z0hrUmpJVDBaUitBTzlGT1dmcHdWRzJqZCsyR09TSnVsSlJ4a2FGWWJZVnRSUlhhekJTVHdQUUNtOFdRUnY3WE51dGR5Mzg0ZGQ2RFpjWlpTdDkyM0gzWG1ucVE1VjFXOVVXUk5ld1NlT1N0TnBFMlozbnRsYVZOZ1c5V0V0VjhRYVk0bWRSUVdZVE9uQmZTeVdBRXN6K3Ivd3VId3VNVHcxZ1doa3FKQUJvdWNBNldmbHQvOWFPbEhiV0NFY2IyaHZTSWE4ZWV2S2lTSTJFTVQ0Q1VLaHRFaC9Ra0RUK2oyR1E0RW9OY0oxRHdjS2orR2tRS1E5TzU5U21FckFyb096THpyb21OTjRlZCtGZHFGaVlSb0dwZHdCV1FaTGdhQUc0Mlo3eWk3RFVNby9MK2FNQ094SGEvOUFxTjdWZ2pmMTJkcjRKZGRmUzRtK05Ud0J1dFBOcERkeVkzVng1UVFaS2taVEVUdG1zbldHUUpKeWV6N2NxL2d5bEFvRm5JODNYUUlEbHZZRHhwb1FHOG1NbVJsUGo4V2tnc2VzMUlTbWhWcGFLU1hwTndSMEh6cDllUW1UL2hFd1VXOW14SkFSRW5uNnhpM3dLckdHYktCV2V4M3RldHVpRmV3cEJ6NTdGZ0pWRFFZa3FSS1V1dnlQRUtJNGxPbFpFMC9oc0lDQWNQalRLY1U0V0Vkd2NZWi9JcmhLdWpQS3RLTEFFSVdacTZhVytFRkx0NDhrNHVTRDBQM3FneTlLcXdSWDlkSDFkbVc4WVRzaXNCTFVSYWJJeXUyNkkyNW9iZkRVSWV0MEtvUWsvdm00MURaNjlVSHN4aHNKbXJVUTVickpYMmhLYVJleldHRW85bXZNT0dNZWtQeFptR05tcFFBT3VncW9wT1YvZUVyaWxWUmVIdVROaUttUVE2VHk0OUFRNGtTcU9hb0RIaDdHS2pjUVBZVlYyKzloMzltY0R6cFNVNmxDZ05vcEhVSmVUcUhoZHIwWG9IOFh5WHBSVDNYSHlQQVhKczhWQlBDVWNnNEdCSEtlck15NTd0MUNhWnc4VTFFeDFWZWE2OUV1L3dZeitadGN4c2JSK2k1TmZaZnVlZFhlS3Bva0RyemhRa1o0VlR5MVF1ZldMQ0pjZUlrRlp6U2IybG9ndGZDaEhFY2JLN2hvSTFMQlNKWitvYUVhc2szeWJNYUN0RWg5dFVxcFN0NEFieHRTdFF6d2U5V2N5aTJQUFlZcEI3alc2UUlSZTVZQkZMUGpSOW1HdkZFL3gxK3VMcFR6aXN6emhrRU9JRkNnQksycWp0bFRObllxMzB5UlcraU8yRzRUcVRXY05FQTRCRE5UVklxZnFJbjI5U2VpbUJ5bGkxWkY0V1RnTW8rOXRjMkM5QUUzbTBGU3ZiSEhwUGdtbkQ2MGY4eTVXWVlrTDFVUzgrV2t0U0J4VTRRWVQvWGJ0QnFtOGhFRVIwS2JFMEhWOHhwVWhaTFBwR2dtMFZBY1hnSEUreXh2ZzZTWmNxN05LNzlndk5lZXc1eVZkVXhlVlR6MXpnYUZzUi9tb0ZPQ0pFVFJhak85QjBTZWptSTdNbDA1QnAzTGMvcmF3Y0U0anVtRitUSHcvSUZjaGI1c3RBR0F0TWNHZkVlczlMQU9semlDY2h2RlVUTGdMencyUWl2eU1GMlJaUklTMStCQlR2elVrbnNRbjFUWTlKOVZHOE5FSjJOcGtFOWw5dDZlZXA3bmI2cDA0enJBY2hTcDlNcG02a2VMdnRrTWVyaHNCWE1Gckc2VEpuL1RpQ24vS2VoVlRlV1R4TCtQSjlnSDk4bW05RzhXdWdRdDJOdnlsL0xtV0xtcEw2YU5ubU9iN3MyTW9COWJENXVXWitHcTJsdVlTTWNxdnE5eC9jaTN6L2lqaitXNWRwS0MzWkVWVEhoRkYzSFljTDByMi9GaTJaQVJRQlkwTGRtdnJXNHFncXQ1WW1SSlVUWVY1eGxBaWhQNnNFMmZESGQrVWlCL2taSFlzRzBuZ0RDRW5DRUJ4UWhtdHRoK3FicUVzOE5KNlA0UnA3aUZvTzVac0tmZHZydnF5L09wcFpDdmtrOVhiaEVEMkZkc1VLbVI5L0NLUkM0N2RXZ1hHa043ZkdsNTUxNFJMcXl4eHdWTnIvTFFaN2dQVDNVUVlVRXJlZS9vOVY1WDlUU24wNEVoa0d2b0RNZURnbGpzM29Yd3JhWU5aU3NseWtnNjFSNUhqT0w3UUlyb1IyRGlKWXZCMGc0cnNvZTh0RkRYa1lBOENyMzkxTWM2VWN3Sm00Rk0vQjFIZVcwUW1VQzFzNGIvV3RxdjVVMk80N1ZIM1dQVGJuTmhCNjRKRmxLYXNPZ25FelhiRzVkMGJvZUtrSi9WU1JlZGx6QVA5enJ0Rmg4NjkzNFdaRERxcFplQWNHZlRMUCtla2xNdGw3NUJDdXhsNVk2SVNIUmkvTzE4bWZNeTQzbnRWUGhFQ1FBMXN2SFhScDlQSTE0N2NTeUJJL2hFK1lMb0ExaGYzT1lNcUoxc3M0cC9kMEpBY3Q4dmNhUEt4MUs3a1hVeGVJSlZyQ0x0THArSk9WMHdnMFM0a3pmNGVLa0Y3WWNCNWZveWVVK2w3RDBhWkwyQmExMVhEMFExQUN5U292alFBVU5ac05GZHQyOU8vT0JpcWZMZE5lRDM4dzA4MVRrc0VsMzk5a3c5bnV1aFJXcnd3bTZXY2hGRkxEbVJ0WUlZcEZ3TVcwNmh6NVdRWTFQdkdJM2NDeHNVcm8rd2dtdGYxMXFSY1g3RERXcUNNRjNlekNrc2s1VmlJWGk0NnNIT1V3a0ZnblVzdWYwM2pLeTZEQXc4THFVRDAxeHplelozYis2bk5mN3UvUGxqamhsVS9sRnFCbXlkMEpHTi84ZmU3WnQ5alByMFI3M1l3YnFjOWdsRHRPQm5qSzdDcFVkNEU5ZElwdUtvSTN6ZjNiTUc1UU1VWDh4WnczN2JkNThJM28rcGVxRGdRLzVsM055azA2bGtrVUQwbnlVdHdDaFZTblpHQmlRcyszUUdtM1FDZkx4bmNIUktNS2JvQ04vQ3pXUUh0RFdPVkZwTkNWRmE1RGVaWW9nNTh3T3VTYXZtYnRQMmlEU1NzZ0lPVHlMVW5nM3h2MHBlaU9SV2FrTVR3blI0eEdLNXVNWVc4R1EweGhzZUFtbE5NYUdIYysrZm1wSGk1QktrNHFGUGhnNVpVb0hFOUh1SzNUN2pxQWxQc2FEVFY4dnB4aFpQS2ZDY1FOS3p6NS94ZVU2U1ZteEFrMmFoejJYMlJGYjlQeDk3VkF0NnRGOGxndGd0NVhacnlGR3RVdFJBMGxoUGViY2xNSmx2UDFnYnhpaFBNMG9hcFpwczRBU1FSU2Z3NjU0OGVQeVd1VE0vSU9DODIzRHBXTEJMT0pxY2x1QjBCUW93a3pIOFYwK05QZ05SVUdBVmZvYVhjaWZvL0ROUUFmdmRDbVk1cUZMUlVGMG1uRHJUSy9wWWV6NXFLSXBYT25lc3U3ZGNrUmFGWXJFWVlQcFpqNzJBVnJmNnlZTWxGc2RnQThROHdjeEwvQjVmejJINjFlUE1QVTdKQzlvemg1d1pQTGYzNHNmdEw2Y01xQVBYWXd4QXpPWEYzVUU4M1Z5eGJiSmx1SzRhQ24xNXArVXR5TlhJblU4cDZ4WkxwK1pkQXVETFBZNk5pMFV3ZlIxUzVoQ2JOc3VsdERBWElDREJZRDg2MjdrOVdydUFIdUg1RUpVZmpqQWY1V1B3OGtJKzV0VzZBUmFZeFBCeVpRaXJSRmFxM3VrdVl2WnMxWnZQTHBQYlMvNm0vQkpBeWFWRm9Ya2dWRFJIVlRaMDJ3QUhhSnZscytsbGU2TFJYSUM3ZWF2TzhuYWlJN2YrYm5zQy9FaGx6TVNqbUQ1eDk3enpMMWsvZzNyMXVGY1RrTEYwenNlUUVrT2pGVkduWmpXVndHa3NsQlJVcXpHc0ViME01S1UrNmJkWkZVc3BIc0xncG9hdnNhZCttWW82WVlGek5oU1FEOVRlVXhWWFBXTkhsUUlvcmUvbWcveVNrNlNkK2ZadktqeEFhb1A3U1dyRlRUeWlPcm1DM2c2eFJYbzhtU1E4U01aYmcxQ0toRWlORmdGMSswdUVSaC9wTVZJM2ZXV1lrNGdlSE9iRkRNYnNZZjIvUDBOcXJVdEl5Tlc1Tk9KY1k2THJZL2FLcGdsamhWS0tDaEZVazF2aU4wZmxrRU5pOFFmQTkyU242SUd1QWdGaitzRHNpdnVWRjZaRkk5VXYzaTRCQ2NBLzZnVVR5ay9vYjFKSmZvbnB1b3JITkV5VVRVbGs4cm14N3NZRHRsTVFReEUzSjJkOVNhaEw4dkVlM25EejNTR3RSVGh1ZU1paWlvL1kwQmMrWTB6Nk8wclJ1VWIvTDhtc0crVWRMSGwrc1Y0dVZwdlJvNVI5Rk10SjdDTHZkNzZ1ZVNvNlU3bE15Z3ptUWhmeUI5Q0hKcmFSQjVuQ3hDZEwyWGFxY2FydytoR0dDSG4vUm9IZmpacm5SSklpb1RJb0VScWpIVjB2MU14bWtTQ29CY0NISW4rSVFxY3RLU3dNbEx5U2N3MlpFR2x2K0xxeWhxb3FHM2swWG9uNFRnekxjSTR4NTFZYWVDOEFqeVdodG52bzRiK0xoMkFYRmRJbktlOG83cHVrakxRT0hVM3dtVEtja0t1SEFRSVZEOXVEQWM0M3A3R1Y3K2Y1U3pjaWZKOXkzZ1RFbFJLMEpYVE8zU2FDOWc1WWV6bTlyd1NGRFZiL3dMTVF4WWM2RUR6eWY2QUNkMEZIQWU5eFNBcElGc05tZFB5Kzh2NGloWnF2OUVBdDJPMnE1eWV2UDE1aDVEWThhMHZRaWZZSXMwVGRSa1FqVWZtb0dxL0EzbkhOcDJIUWZXc0JuVjNSdGF5eGVqeHlBZzJ3aDFmVDJnZFpjMTkzRVBoN2NwTDFSR3dkUlZtRk9CSkt6eUFlQk4vdEswelR5a3I4M0dlNzNPNlZkTmliWFBmTHU5cmlBeW9WQmRhMGdlWnJJQTNjU3UvMk8zSGxJNHlVWEpvdUVKbU92WDVtVXBrRjRuS3pxZEFSelBaV1JabXFtaTMwelBkNUxGOUJvNEtjWlNKblF3aWRHWHBLN1pRTWFlL2pMVXIzcnowSWVUNmY4S0hyWGFobVhKcXJScXAwcFNZcW4wMmg2dGVOMHJEQWRXc3duWDNWSDBqQytKZDB3ZW11SlRhaWhSTStpTTJ5a1l2bGU1QnJpS1oreGhERlYwVFQwSVdhRjJKM2VFaFVqWnZCYkRNVGswNC9vYUVMNDhqOTdSL2t2RDlaQ3cvRFhsdjZ4UVpHa2FGTEpiVzdYNlRYNzdVNjlqOE55emFJZFNaZVFpME96a0NRZlEvTFRrSnp6NXVyMk1aZ1FBMXdmRVlwOUFveGczcDU0dWxSTFVzUUtjWDlYQ01RdzFOdkZkZU1LdmVoYzR0ZGxTbDdQdE1NN05xeGY4bjAxd3ZRRGZMMTRjazYrbzRlZ01XVldoVjhPODhSOEJJbm1UN2FJZE8xYTZHUHl6RjZKZmMyTlphaXhqVjJaZFlHdUpxNHlnSjBKZkg0bUxUNkhoZ0ZTVlA0L0dsR2NhQXp3VGgyelJuSW1jWm1VSWhqNktDVitFRldPbVN2eDhIYUlOSVpIL2pVV2hQR2FsQWx2bnd1OGY1eGxGdzFwVTIyWkk0bWI5MU9OdVRNQ1BYV2RJZ0h5YkRVaGxGdFVaZVdKUENscHVrOVIxdWcrOGpMQ0xzV2pmV1dPSnp2U2pFU28rdG5LTldFZXlvTUV2TEZPZmtUT2xTRFU5L1JsSVhOWDdwd0Y0YnhRVTd2VnlMQkxXM2k5RGNHbFBJTWk5L1p5R2Y4ODN2T0I0T0xNcTFMckhRdEEyZlEyUHRtODdxOGFwY1RXSVJjWXNsemdTY3RqOWkrc0VGOEtsaUJmTG5MVzZHUlVkVmRMc0dUZGwrcnNHREdQaTVtU0RXQmtYc01BRW5Ybk53UldZUkRNWXZ3YVQvUFllZVNBbWgrOTN4ZjJYeEd6OUd0eC9oQUw2c2ZSOXp1ZmpGVFR1S2VNYmlmblNjMVBmRkJQWXdIMjRtWGNYdnFwV0lBL21VUHN6V2dxTGhCZVNqQzkvZUtzMEtRODFlcTkxWGNQRmxCa0paQ0dmY21ua29YWktmQmRBRC90MFhKdjZMV1F5ZHJlSDVjSWEvdjROYWNiNW93N2FTdWNyR3NRS0tIdWllZEdac2M0bWhWWk5Kamp0Q3YwWjdtR2NpdDdwVHlOWEZna0xUK1Q4bFJsQ1F6eHJFWGJHd2Y4U0t3NU9BUUVOenlYdTJ1MGxFbW1iNlpRRWZjN2tjeS91WmlwaitybURrRGwvTlJjMHVIM0xJa3JJb1VMb2VEOGZ6cERJLzluRk9nQ2ZlUTl2cWRMdmU0Q0JQVC80bjZtOUpNUjA5bVhIL1BTdytISVR6c3QyVjB5Y0s3a2t4bktzcjRCMGlKRTF0RUh5V1ZCVzNZbmwvS1VMKzJWcm0zZTcvOEdaNFh6VWJqYzgxa1VKQjlKYStYcUVHelZleldIcDFDYXhZV3NsbFozZGJkVXl6a29lcWdxaTdjbVRzMG1xRFQ0MlZoYm1SbER0ZVFWN3NQNXNBVjRuOEdLcWdrSU5mWC9hK1pzQTNUYmZPbXJnNDNpcCtRMVdyaWcwaG8vcS9lUUVpclBlQ3NQK3hqcU9YazZpSkp2TDV4MW05SzV0RjAvaEp2bmtwRDQyRTk3WDRoT2o2Y1kxakh4cnN2cHlKZUJwSlJrek9tRlFkSzZDR1I3MzBFa2doOGdSb0VhWFF1QUwxK3I4RE5zK1ZkdEZGMzk0KzZ0VWZLa3J6ekJxZURwb3BEdTFmYktjY01LS1VhNThwZ0JwTlZNVXl4ZFNSNmhHWnpkQ1lXMFJpQjJna25xVENHUmYydWM3aDIzb2R6MFhPbEI0Z0JCYzBTTVlmaWYrS1d5Rmw1YjhWbmlQUTZGbXpzcmZya1ZnRkowbHdZT1pKQ3JpY2Z1czQwZ3R4OXNEWTdlQk9iSW1YMXNnVklRT2JUeXE1WERvTTAvRGpqcXpJQTFVUXY3QU5ENmw1VUxTN2QyWCtWZUpFUzIzSHovUFJaUnh6a3ZsZWQ0dEh5WmVjZUkyMHVVd0M2VUszMTQrTkNSU29QdjV2Z056QmJ6NGVLWlBEaFY3RHlHeUI0UzZhbmQxcDI3OXNWTUtwMXpDSENwQXhJRUhLSHd2S28ycmJBWkx1WlQrbXAvWTVDZmRacEpNZzhVMWlNdW1LVWptamU1dmpNQkxxN3hicERjWW5Tam1YeHJkZUxNOFNZTzc4dmhrV3kxaHA5V2NYT2EvZ0Q0SnQ3TmxWc3pleHdiVDJGaDI2S3dqd3ZsREw0L2J2N1ZseUJyUFBYdklVb3pGMHhRU05QNU5BdnRSTHRyYWE0QTlqVDBMRmsybmhhWTFhbjdJT1dib0t6d09tV3NXTUt4dmxaYWNmcXdCalNPSllXdWJHS1NmYUkyN3lGQ3hVcHkweUVQMzNPZTdZUFR2blo2ZDYvMTBiWFVaUlViamdVUWlFZitxRytNRm9DOURaQjdScnNSYm8ybnRqS2MyNEtRcUI2eFRERG9zT0NLcStUU1ZuaC9oSjYvUmpIQ2hvSkFtN1dUQytuV054ZmhleVgvNEJiemlsWDg5aTFxSk9HYTU4ZXpSdW1RYUExNEF5Y0xZa0JJNWgyeElJSjlLeC80NG5GNEpDaE42ckxmNzR2TkVTeWlwUFF0ZHF6ZlN4dmx4UjNVRG14QTZoUVVKQ0Z6NmJYTmpTakNaV0I2SVVRd2dvZElZUmFnVkh1VDg4dDFTTyswVzUvVkt0MFl3dVArUlRxTmplZElxMmVMbkNzc0NHYjlaL3dLN1lSc3ZXR25EZDNvSHNadkdQeE4vekNtam5zaE5Tb2dlQUtyUDNMeCtCc3JvcTBpMThaU0FMaGVHTkplSWFmRFB4dlIzUVZvRlVSZmM3b3J4dFk3VDFtSnZOZmgwVGIwa0FEeFFiYmhHYUhjSkgwdEJvQ1c2VFh6cVBiN29Nd3VmeUV3UjA1emJEYU8zU1NnSmh4dE53R2ptZnVEdHhlcjJjcklUYTY1NDBtdU5aY1FzVjNYeTVoMmtHM0VpUllrMHl4amowTjdBZkFOV2VROHljdjJvWEx1MzRkYUlHL2dueU02Nks4WGVub2hKZEM5Ukxaa1h5KzU4c0VXdk9DWm9vd0g1M3UweFhyNGROWGtPL2VZbHNNYnFobTdkbWRtSGMrYzFROXpyTkRURVErcGFxT3BuR01iNDRqalJFbjd4aC9ERFNYTFc5dmFMUTFPUWZYNDFESDFTSWNWZFlib244N2FRZVREV0VFYXdsOEtIeEVscXJVRWthWXAxd2txR1c2MVlheUkzcXpYdGtQQ2pqckJMOFQrb2ErRjlLam9US2tEY0dMUFhHTDBCL2NpNWJicVJLNG5GMmY1ZEJOa2UwRnI1Y1lIN3dPUHdBSzhITkJ6aFNRN3VXMTRpckRtbllZcjdNZFRFNXk0ZzlrclUzVzhwdEVmMEdNMWRranhUejMzVU94T0psV01oZW8raHNxbFp3NURmUWxQRXR5TkcwSlpDNTFTQ29lTEJiRHZTRTMvMWpHL2poSjh4VFBBUWRwWWVtTFdENHZ1UXZOMFB6TEp4QmxmQURYTnQ1QmRIWkNIRHE3MXplUEdKSE9RTnR6L2w5S216WHdib3Fhb1prZlV1ZE5YWFJzYzlnMnltWU1CZDFYeERxUFEwSWF2R044R3hZbTM0ZTFVckQyRXFFeU1ZdTNuQkUzWk1Yc2ZjOWxZanRWUlJQNXp3ajVRcVZaR1d2cEg5S3F1cW9QNmw1VmplWCs4VnFNNlV2QXBUMjJ1MUxzSzVqMTF5ck9pNWVGVEV2NlNaSDdUdFpqamx0SDhQR3d4OU1VV3RPM21jS1BKcm1vbWViRGg1RlY3eFVpK1V2akV4TXBrc1JDaEFEN2hqaHEwcUlsaWo5QU9aWExpejQ4VXFXbEMyN3V2L1FqL0Z4aVp2MGl3TFBURG9WN25SeUhpUTMrMDBxZjFKVjhQdU9IenpCN0Z3ZTByaWhCRnpxTEpoSGJjdzRzazV1Z3I4Ni9JeXpwTGUwMFVRTUtSSFlycXAzOTZNeUo2bzMvMFVSbko4N1ZodjB6V1U0K0pPOXkza0ppajMxcDhsQllxZGt3QVhFV2VSenB1UkNRWHU0MDhHSklqRDl6a2FORC9OcmVVUlVUUFFPWFNQajBiT0RJSUNZNEZ1d1dOTGIrMUM5TWNBdDRrS3Q4L0FTUkdpYVMwbzBSNFc0WmNBNEI1TFpEL3BPaDZMMzV5UlBvMjFWaFdCWkI4Z08rbW5lRHdwT1VQemMzaTV0cy9tMFpaMFNvZW92ei9VQWlYNTZDWXhLQ0kvYmhmQm1ET2xYcEhBaVRXRzZtVmY3YkNzbmNpOStzU1A1SGpvRkJDRjRsMDNDc0tkZ3RyTTZmVGVnMGNINzVBYmJKc3JZeTNocEVocUQ1SjlvQzJrTnA2RStXNFgzL25ReTFDNUZISzFURzhIRm9RZFNWcnNKOGg5ckJYUUN4MmVSZmZhYVh4R3U1a2I2SjVZYklHQldNZGRWazZXalhYajRLb2xEcERGdmVManRYYWdMN0hacEVkUmhyWnA4SzZsdWJhSDc5VEhDK2ZMcHVaaWJsNG1CSnZzZFVOWWJ1aVZVQzZDcEpLRThKTWY1amJ4Z1ovNnNPMmVyeWdHeDd1UzcrcDM0R3EvRGJscFdBSENYSGQweGsxdEhta2wxTVdPNC9sUzVNcWRIV2lGS0Z3NzZhS3k4NDRYWkw2VFNZbDkzWXJyTWRTMVQ4TTFGR3RJSjlINng1Z28zbnFGOG1iNFBBTlo0aFQvVnRZOUNHdm5vS1VySXhmT21PUmpycHBmRDJHT1FiTlNhZUpId1NFK09zYlZDMU9STGxaS29HNGp1eGk3MUVDZGIzVlBNQzQ5TzBsR0h5V2NtejFJQVNFWjhQdHFpNkhoVlM1VjhrS2ppUDdHY2Y4ekNrdFUxVXpybjJ6ZGphUkp2ekF2b3EwKzVLZnBEVEYzdi9GamM3MmFkN0ZUVEhCd21adDU0N0RVei9NU1lKMnF6UGtEeEhDb204T3JwUXVodHNnc0d3RFpjSENpUUZMeXp6VmpVdEFTcVROaGMxYXpsQ3phUUhaQWpmR2J4MjhPTmdZbW12TDZ3eEw0VkIrTVpvcXY4a3BpcmxnS2l6OWVxeHhUdnhFYjBKbmx5Ris1WDh6bkJCOWFDUnFCaERGM3BINGdoNUpMSndiQUNJSkJtWnh1TER4VGhzbFFwSlo3MXZKK2tEWTg4cm5HWFExVnNoNlNYYVB3MG5FZG4xbjBrSk9HNkRaZ3lQRVQwcVF2NVIvOGxwYVZWN0RzYnZrRi9JSjdjc0JQQUxodTJOb3pDMk1LWW9wSm1VRnZLK1pYNFZHYXpnNlYxN2lFOTFtTmlVeUxZWHNrK1FBS3haNGNwMUJ6eHhjTDNudG5CeTdTZUxBMVpMQklkdDBtMW05Sit0aCtNRnJwMkR4aTlzWW1CMVpsUnJ5KzVUWXJTL1BtNG92VmRZSmhGRTBTcnVQOE93ZU5LWG9hN0JPN3VwL1FybXlqMEVZNUJ5Uk9JSSt0MlIxMzdSb0FBVUJ2dWNwZkxneHVWNlhYWTRPVDBOOTZnbGc0UjkwUjFDZTNmMjBlbHNxa1NVLzZIUnNseXRIMFB3cVE2S0hrL1RhclRJeE9MWU5RK3JwT3U4Z09TQ0ZzZzJ0ZnFEaGJqOUtRdlVoeHVhNXlSS004OUQ2SW1tWEtXZmVZd3ZMaThoSXdycW1kTStzRTVnKzg2ZEw4QXZLbExVL3pnRitVL1dsZG1wY0hLUm5vUUZ4U2JuYTEvSzlHUVRtbzcvNjRuNUVmYUwxRmM2QXdIcitldzQ5Y3M4Sm1YY1Y4bU9tL0lmQktCUGZ4V3dCNnZwdFhNMmtQOU52QnozY3lWNlJNS1l4d0x0MDZrdmVoQXZsWnJmNzVaZzR0c0VFMTRwNlFJUVQrVE11UmpDZmJZWHg5Nkh4Ri9xMlBSRUJMeWJQcXNPclBTME44R3kzaU5kTGU3NjNiUG9WcTBJSTNlK3ZpQkhaanJxNjVESGE5VFh3MWxUbmJaUllwVzlyWHNWNG9VZU56Vk1ITEhYY09vYi9zMlJBRTA4ZVFDNGREL3pqWitJYUhHajZhMkorWlkzc0RIZ3FJekg1dUlKKys3MWUvbjQ5TVR3dE53b3N2SWlGMUNWOEdhMnM3UjJRNWJBWHVhYkRVcUp0Q1lnaW1HQWU0M2t0WHg3eVArVTlkK2VaNitxa2lNUCtDSk1HbE92WVRMOXJQY3hoRy8zQkhDd25iM0h2a1F1OUFJb2FhYzVQVTdMdnpYZHdPSGY4T3hWUnpMOC9qcDlWM2lMelhwRkF6akh3OHd6a3IxYmZPclJnRFUwdy9DZnpuYy9EYnRENjgvYzdGd0JIRFdPVGNVbVlkTzc5VU5ienFQL2xJYU92ZGNqNEhOZHkzSkg2ck92bTFoRjB0MVdwUGluRFNHWU82VklCYmJ0TWpaSEs4KzJ4K1p2ODFSSHduT0dmVHRxZGl4eVduWEFWdUlZd24vNStsK0VPMS9NOUlHVWdIRlhGdVZiSzZ2ajN5b0RHQm9yWW55eXYrcWxldTAyaytNa3o4a1NERVZlL25jMkdYb0IrMm03WW1pNkdxRG51VGVoY1BhVENrNGx4bi9YVjZiTHl0ZnNxL3RXVk9ZNnZNMmRoSGdEMXJWK1UvK0E0QWRtUTlhakhPbzUzbnlnUllMZmJtaSs0eWtuYU9vcHZzTWNFRTRRWDNHaFdZRXl6SXRodzVZTW92dEE1ZDVMcnpTZXY0bmRTVll5Wi9Uc1pRT3lLMUpNcXZYYkoya1dLK3BDTERYQVBYaURoRmkxeUIxaWtvd3NNWlByM2RyN1FjeUcvRTd3RVQxdzFYNDJBN2l5Qzd5V2pqblpWajVMSVhSZmhIemRhRG9MMUxKM0YzVUxIZHJFTXUrU3VVYzdJQXFQbGh5anEvRkc2RUY4cmdYcTBnc3M5aEpPS2QzZzgrbC9pQkY5dVowRlpGR2FWckN5b1paQm84emw4SVU3SFl2YTN0N04zU2d3RGltUDFXVkZEeElCOU85TnhraUtwcUJTV0tXaFpuR1JQUTNqZmdZNSt1N2dCNTVrdThhelBWOEhWY28rVjlsdFdwQkFtRHRKbkd3QnhJNjZIYjlzZlArMEJCUjNyUkQ1Nzcvcmx0ajkyOUVCK3FJS2I3dkgwTHB2SmhiaWV5WWx1eHA5NGJEaGxaT3YyYjhReWRBSzVzeEZscnZyMWI0MU0vSDFieUtLWFlVSHRmb25jNTlEV1RjU2FSczVJMTVscGJqZmhYcHhOamFKU1F2T0dBanUxUlp5QStDRzduNmZ2Skcvbml5ZXJCWm1wSXN2dzZGaGsvaGhxQnMvRS93Q3NpajZHbng0V3J2YkFVQTlId0xYbDloMnY3em13RzFLODdpZWJUWGpVQU03bTArSFI5N2dtODBTWWtDN1VEN3ZNdzNHWWlya2w3eE9xN0xrWmRKenNwNXlQYUVxeEw1QmtmcHh6SWNySnEyem15VmRrTFM5WFY0WG9MZlh2T1FXMGpmUHBTbWZaaGgxLzhkdGNnaVI5eHpUWTR2WkZIZ2ZVUko4cTE1V3pjdDlCWTMxQWxJYWZWOG9xSUpSUHFmWlloK2dpeDdnaFdwY0JLUmhBQjFwV3NjMi9COEkwQ3hVTENRNEVTSlg5U1FTc2h2Y1VpUFYrbko5QytycGFMSXMxUXgwZ1BaZXppdTVvSFZZTURJN2xkY21xUUUwN3lOd3JMOGFoa214UWZzU29ua3I0M3lUWFpVQzhyMll1R3VOKzI3YXk3MTBleVNWL3YzV2N0N0htWThyZm9VZkYva293TzdXbThDQUNabGdSVWxJQzRnSVV2d1FlaHI2T2diUVRYVnVaN0QzVmVsYUloekNWa3NWcE5tc1QxSitERWdrcnBKVnNXMGdqbENvR1R1ZlBOQ0MvSm9tb0x4MUVlOWdXNEhhZnY4WDNLVTRpWDgvWkRJTmd0WGZucDRWMFNPTzQ0OVMxdGNuQUF3Y1lma0lpcUJSbDdld1Q2Ui9ZVFg5d2F4ektrMERHRjFvL2hiWGJMVWh2N0x4VWp4MHJhNWJROUJvTGtybFFqZzR4c3RneGZWanNWTXNaTCthaFpwczZzeVlUSGVXYmJHWVMydTNqRmdtT2F3VkVIUnNQaW9OdWhGTVVxZTJjWXprYUV1UFB1eGoraXczQyttVk1FeTZCZ2pJalZIdWN6OGpleDBBQVd2MytKOFhoa0FvT3hIVk1rV09NSGk1cUJlQ1hkdzZWcFFMUFVYaERRVkpmTUR6SzFoQUJHeWVuODhqOVpGcDFBNU16RVZ2VHMrQWVFMDRDcW1ubGF6M1p4NEt4S0VzaFcyYWpNZHgxdGczSlRLTC92VldjQ3lWY3creTFlVUdxaUVYbHJuT3p6SVJNRk8xb2l5SXg5ZTJRRDFvT0hwanBqY0FSa0MyVnBYT3FlZ2t5WnlGdFVoNXlaVSt4TVlIMU9iY0dmdWFWY2pHNDQ4WStUTUhhT3N0a2M5S2NISldKdExqV040cFpUT0JUSllEa1docUVzVHZpT1BhQXZyN2d2ZndWWFZwMWRoMFBVR1daUHdYVTMwR2dvVWI2VlFMNHhTcXEwa2hCS21VbkU4ZlpMUm0yZVE1SmhuRVRsakw0SFRRdDhZOENZajdHdURzS1c1S0hwaXBnWFpBNm12OXhMaWdzV21sMWJJUWxGVkJ4NGQxcFBQQXhycGhNM1ZJd3lsV0dMb0JieXhQdkxETmduMlBmN3VzSzRhNFk2NEcwVXVlR2tIb0NkZ093NHgyWng3TlJUTlJIWHJQd2VJbEFla1JjbzJiKzhCaytHSHQ0dkFkbHBrRDdUemtzSmZLRkc1My9iOU1DVWpwWEppWkVQY1cyZzVxNDNLN1VHMU8wQklkU1Z2eHptOE9zU3NsLzVicFRHVDhNSnVMVUtTVTh0eHA0dWhUNGNIR0FFWmxWTytGakk5NlMzR0YyRlJwUXRCcXVPbHBEUjFFbFFtSTBVWEFWNDZLTll1NWdLMU44ZjYwMlhVb2VucFl3a2hhSnpTeW5PRk9tWTFMaUdDL2FycXRqRU1SRjUvVHIySFhuVlRrRU8xdHowMWdqRmFBMkV4OUFXWVRnZk50eStXUWpWS3drNmx1eUxJMzh4L1RmbVNLU0hEQU84eXdpdXpWNjM5THBheWQ5ZThBanhBcHJkRjd5T2REKzhQNGdBT2RudHBHYjQ5Ny9mZnBJWkMvQnFIU0k0TjFhejVOZGhCY0NRRGhzdHRSWmR1VWZhOXJDdGlKTXVvRGI2Q1dqMG5LUncxbXV6TjRud2l2Yk1GeDZSMndhZi9yREJKd3Q3aWFFRnUzRklEZ2FnK1pxdkFGdEpTZkxIUnVnc1VkdDYxeHRiTStOSHllVmZjSlN3QTVMN3ZOM1FuN2lpVElxKzVwT2RETmFGbUp6TEVNaUlPR1d0dXM0NndXNEpLQ2p2TVFWZ1oxdzN5b0xGeVpmNVNMRzl0RFFqUlJZK3NjK0VFSkd2OTVSVDA5OCtzOXRvOUptbHNtN3ZJRXFGOXJ1dlhzWEZPOUJSclZlVzVoVWNGTVlQVTNEL1VvZUpUbUxDREZSbDk0eng3RS9uNi9UWUthOTJzcUV3UmdmNXFZWTJsYTNzYzlYMmRZVElQdWlkUWNkS0ZzZTkyUHlhZGg2Y2lHbnB3U2p4ZmxMWnUrckhTd2JEdEs2cGNZL1FTQVoxdCtSTmY3Q0k3QnkxMjdpdFVNY2VvTlZrR3R6akF0ejR5WXhUaS9KZUJWMGZ1UjE2SmpUdlN1WEFCd2pwbjAyUmJrMkoxN0tzY2pub3l6VFN3aWlTZ2F0MlVxQXhVYkVkeDR1VTgwR1JVcG5DVWJ4OVh5RHE0OXp0OHgwMDZpZWhPb2JHWUN6WWxnS2JSbXpiZXBTbVNYN2Mrdmp6bXRkbXNtVlU2YUpoenNOdkNZL29JQlRWRE9jMlhqc0VIdzJkYWxpTnlBU0UwakZUdStVVm9pV2Y1Skc2VFdpNjh3Z0FXNWJidWxaMG9uT1k1V2VCMTdrdmlDNmFPYUlKbWhmR0hNa1NvOUd4SHlSS1FiSm1TTk9iVHBkbTJycDc5dWFSK0MzNTFpODNmOEdUeUdxQVVOakpHTlpMakQ4QTVWL2IvaFJnMFdRYlE5R0VYVTdjcFZTTS9MbUtuNkdiS09MaWlsdXRBRGFaWVJXN1ExTi8zOG5kUmtXdE0wUlp1NFRHMGc1QlVLb1hIRUJCOWtBeDlwWklXZkZxK3daNmI0UEw1c2VjMWNDVDR4WE4zaFgzUGJsTGZiL2ZWMzMwc1gvYkg5OVZ0YXZodEpISkpSQi8yYldINE82dVZ3dTVjS3JNTm1IK0psa2tPMzFRVmxyamRmblpqekpMTzVnR2tUT2tPVy9xRjE2ZXBKa0djQ2Y5dWlibUp1UUVxbXU1c3hzalV1UzNvWjRKdytPUCtEZEJwQWdRNGlDa1d3Q3Z6R1ZKVkNPeE1pYldNa1RJdTRRMHRoZ212RnNYcGJvcmJCbFF1bDJ6Wm1ROTM3N3V2aHdFT2JwOVFxK0pkazJ4WEs2NmpyYW9SNGw2RXlHMm9oN0w1eDNaSmZERmhCaUxoTHg3UldIak5uMGkvUFVSYU03TG1pOTNNcHFUdzNYS0dJOWdGU01yZkdlVVdnUS9uZURQWVRONWo2cnlFY1J6TU0vOWRnb0wyd2tsbCtubDNvNW91SzU4b2loR2p1cHdYbDgvSGhLZFVhc3NQS0MvOVNqR2RlVXE0bDFmb3oxTWJNNHovang5QjB6VEhpUE5KRVJhc2tHUzdqM0RPMVI5bnRPQ0lSQUxNM0RobUdRNXZjbXI3U2dPUGdWNlhnS2tmUEM2TUpLQnpoRS9idjdoWXNqNTZGVkgrcmswS3VmTFdSM3dNZVJseHUyL3VNcWpkVHJvZ2tIS0ZHMFJ2M2piZjRiS1VIUHJUckxRZlhTV0RhSXZzQm5WT3lQa2UwM1NEVGtjRVBPaW13ZjZyaEhrWkhScGUxYTBZNUtPWWswL0ppNURQelovRlFlVlJhWmhKZG04a1FZQ010ZzlNKzlYV1N0dngyV3hNSSs1algxa0gvcW1YczBiSGRia2tzQ3V4bEpHbFc5Qmh3Sm5ZL09YdW96MmtnTVNRWHdJbWxocld3eUVMZ2hEMGw4QXBpeTRoM0owTHR2akp6SWVIUzJ4WUYzSXVFNE15MjRYVmY0TExLcCthbDV2cU1pSkRGa1pRTEdvV2FJYzhmeHRRSVhBeG9XTFdRdGxMbEpodVh1bzNsR1JRaGJQc0hkdXVBK0IrZkJZVWUvUitiOXc1S0k1R0FXSHNBczlQT1BLcGlieXJxRkhqbDZSMVlpdHVFOHJhb3JPV0ZlT0ZpVHlLUWs1b2t5cmpWU0M1TUdPcHZRTGNBdDJkM0ZMQW5QR0JPOHl1T0tNSVVhUURWT3pibmlma01aKzFWNklRRlAyL3Q3RmcyU0dZSzVVTmxyck8zUHc2Wk8rcVRxR2JBb01KYkdvUHZsbDFORGdnSmNSQVpUdmc0dElORmVmYi9VWjhhSWNSMms1QWUyRHdkWC9RR1FBWHQ2QndxZFZXQWdkMUVlNkJab21Ha3hIVnlqTFZSNGF1dE1ha0pqSFpGUUtnTTA0MDhXQS9xN2ZJZnBZOWhQQmFibkJxYzNBRDBEd1FjLzJyVnJZSDZFZlRPTnVFa1BLZEVlY2JpV0xtYU9NNjNRR2V3S0NFdXhENDNERnNMbUEzQSs5ZkpBKzVOVWNEcXhhK0N3U2d0ZEdHdVdKZUdHb0Q4Qng1QW9RZ0NrempuUXdYcS90dWZBVS85TTJPS1J2eldQRDFnNDNDdEZQeGUrNml4SUhOeENzdVdwbjZaSUdxdGk3bGRRaXlIZ3pnQTVqOC96dFBKcHhjVUllZmtWd3NoWkx2aGlIalBYZ1ZrQmdBQnZVeVFtSlVkZ0dQMXlsR2xwTHFYdVRZYUJ3WkEwMC9IU1dMMWpqa0xOdWpTa0N1Z1Qrd3hUdEFKM25rTHl5b2JZY3RBNkVTQmdBYzkxemM3cHNYVXlaMk8zR3M5RWJJd0RJM0U4RWc4TUN5c3ZSUHNBNVBZY3FTL2xHcG5MeXNQNHhZZnNtblRqNzNPcW1Vbm4xeVNTQ21mbVVsaENlcWl2eXNUc0g4UndmczNvWFIyU2pwQWJPSjdVNmc2YjNnWERoMytDTVM5emhGbEdMTVhQS09xQlVLME4yaitqL2VPb1NGTFFwY1dydjZwbE41Zm15MC9pbnlqTnhvSDVrZ1JNa3NCUmJLTG9GczlOdHc4cGZyOTUxaDhYOHU5cTFoVmNNZmlyc0JxV2JUZElXS0JjMnpwK0tSU1dFdTdmaGdPWEVkT0UwcllzSmZWQ3hWTDNUWmQrbk1FM0cvUkJNVE1CamhZQmpZbzVvT09qWEZEOEVQZ2MrUTMzcmpyN0VMRXEvT0JyanRGMldtVnkvTlhRZE1mb05yVU54dEU3N0hMRGpET21iMC9lemlmNHRiSHpjcGNHMTlCaklKVnFNUzJHcG9tNHVjVk5wbE1aVGNiaWlTVVZlWlRXTjNrM3RpL2VOMGNaZFMxRW9Ddlo3LzNTREdReHlmM2xwTi9mQ0xhcEtKSEt4M3Y4UVZWd2hqb1ljbWJnc0xCRE83YStRa2pTUU5BdHRSOU9BUmpnN09sc1NSeFBTYWdiaTF5SjNTTk1NaTdKVithUExQbko0SjJEQUsrN0JUNUN0elBqSzU2RUo4cDRnSFN3dzZEclc0eGVmZm9HYTJHNGgwSGNtMTRMRE9LLzlBd3VUeHdwb1RVMDc2NTZrYk1ubE5ROUxBZFlMOVh1elF4RGhRVFRYc09ub2RvTkh5Q2gzbExOcUNFVGVRL0w3bkJZOTYvQ05TQUZYd0hTNEdpU1VwMEowRWVweUdVZGMzMnozWlNsNVZ5aVNwVFdmU0Y5bDFPa0RNU1paS0s2aVFJZnRyUFgzbVFMdFRaRXQrei9GVlZvbVlvZGwvb0s5TjNkNXJTRGl4Ukxtd2I4Zlc2ZEpyN3lIbGt1Y0JsYkkwNVBWR0YvSE5yMEcrM3c2VVVZUzNQaC9Jam13cjQzcEhELzdEbGZUQy9ueHBXUlRXcE01Z0FiRTlqRll6T1dJQzdQQVpTQzlYNEp0UGsreFdXR1pEZ3BvNGlYV1VvbFphVFh4S1hhTS9JTmFaVTkyaHFLMzhST2VwRHNLWnZQU0pKeFpnQS9kZm9wb1Bkd2xKRXBma0drMVhqdHNiSTF4VnRpcXlTM1NReGRLM3JUbzJpdnp4NU11WE5JNjlLV1czUVZzczZVWEpxbTdMbWljSExtZlBGb1g5VTF4dnlxU2RYSFVlMENLWFNxTkE3TGZ1eDlVQXRtb1Fsc2J2L2NWWFVma2VFOG5pYWpLY09EQzJGbDVOSzRBeHdGVzJVWHpkUThIaXQwMTVQL2FNTHNCRTBnSFJUN0RhZzZBWWZRZlliUm1kMGdoOHB4VHZOTUhhZTI0cTJvWlRBcWU1QUFNT1ZNQXU3TjdrNzllOStzQzM1b2xlSWNqc3Q4RXlGQWtOaHJJOFQxR3lYekoyeXh4UzdJTmJZanlsS3k5QlJSeWdmZE1mZnpJZ25xYmhLRGRXd2N4QSsvMHlrdkV6L3BHV29yS09xTUltMzg1U0xHRWR3SFVOWlVNeHE1L1NVazVKaEpOZHlyMHNLWTk3dE9leEQ1VDFNYmlXemFwekIwVWZmblFBNExySnBZcklOV1Q4L1ozZU1VOHJLOXA4cUNNQm1OTWc0RTdxOVZjRm1uS1ljTHg0cHRyUnRsdllZYTdNSWhOcVppK2xkckpxdTNtQ2p6d0lxaUJyanJFNjNMZVdpVktqN0JTc2Ivb2JhZTBQWkp4MzFabk5namRzUVI1RDl3Z0FTTWJUWXdkeDU4TUUvZWptS3pXeVJiVVlZenlVVFI0a1A5d3ZWZlRDZUVPSHE0WFQyVW5qQ280YklRaHRjM3cyUU5KV2c3Ymw1bzloUmZoWHpwcEIzL0paQW9Ca21Ybk5GNkliTnZlOTdpbjRCSngzVi85SlZILzJvSncyOG1mOEgwYVhFUjZBQXhhbGQwZHhsTlQ0RWJYTXh6V0w3cWVPQjM1WFd4d25xSXZMUVE4cUVrRlA0VDdMVXlKVE9uVFVJN3Npb0tsVFNLY2xoVGNCcmtjd0VYOXk4QUNWMitDYnZoNlpWUWs2RTdPUlZkb0x5OEFrd1VTRkVxWHVNWUFCS2JHQUVsbTVBcUFDSllWZy82NWIxWGZwSWV0ZjlJeG5FTG9hSEF4SThTUUhoZ3ZCQlhNdGRWRTVFQWl3bDVCT0lYanlaVTZxK1ZXQXlvNWVaVCt5NHUzWHJEeE5jaWdIODUyNEZDdTh1ditxVVp6N080aDlXT2JkalRyR3BKamx0TWg5TERLMU1ZZCs2WksyYWZsMTBXaWUvUkxEWDdGOGlUYkRHT2thZGVVdm5OWjRXZzBQQS8weEVHNzBpUjREVityV2xOMy9vNlR2dEZuNGs3VDAxWlA1Uk1vdmhNb1dKU1VnYXNSS3VSa1B6VDdQWisrbjFWcVltbWlRTzh4M29jbWlTTjQ2Ync5TmdXcFIzNldMVkx4ckVLZVcrOG9MZEhqdm5uNm1tZVhON1VKYnN0dC94L3J1UXR5c1VnbXhGbnJLc2F1REdXb3c1MkJQejlRbUptTjg1bXFOYTBBMmlPM3FyR29hWEVGajVaelVlV2pjOXBFMm9KcGNXbldRdS8vZmlucHJ0VW04Q0c2UU8yakNrME1wa0NSQXBsdzZJNjdOeUc2ZVN3dWxjaEFBOGt2RVlsNDU5Mk5uSkI3Q0RpNk1JeUpGckpGNmhjM0MzVjNVelc2Qm4wRnBCc1M1aURSSDIvTzhGdzdHSU9raHlMU0tKZ2Fld0RTRkJiNnRDNTA2cEg0MitkUGorQlpDRVhjdml1S2NVWHhSTkMyTEdvM2VVRXNJZ2tFdld3WG8wMEZSTUF0K0Z6RmdDMTdnRzlDK0Z5UGNYS3FhMVJtbGpNaGp2S0d3NytGZlphSmxDaEFTZ25yQmtKZ1VyTnRXNGdjRDhZbmNQMy8yM21iaWxrYkJpazk4eVpCZ2xSTi9JRjdyWjIyWCtEQnhFTnhwbmtLVUFKVTMzV0ttUUlseWw2VDVsRGdOQ2RNU1RkZVhrd0hZM25lWTU0bTNFRENqaXYra0lTYlBtMUhaUVloU3pKZjVnb3VZdkFzMDBzT3FFc3lQK2lnSldjeGQxWmRSS0VKa3JHVUlqYkl4YzdlZkM4a2QzdlVjdDFrbXpFajdNcEQyWjRjNDFld09wQi9zbm5RM3hSSEhDUVF0czlEcWVaR3d2Qk1Ydkp2TXJOeVdIZlZxVHYrWkdGWENDSm5FLzRKZUdGYWdaSjVVengzWmtTbnp5aklaOXh4ZGplazhpVTFNZ2twa3BoR3JxbURIQ2gyVmlzMXhZaW9NVitQRThaOU1mTkZ2ck15RDFBQTlHRTZIWnBuL0NIUHhTRWZBNmk1ZnU1WCsxODd6Q0ZaTHdzNUREcjlHVCsrbkR4U3IwQjkwKzZIR0VGN2YzaEZCeTd3aVlRbldON3FUUGVmdHpZNmJJM0luSEFHVmZQSk5mZE9QT2tnQmdKSjA2WnBKZ3pVWjFONXA3V1F2cXFUVitZS09OODg3bElxaE1GVWNNUUJSSUxZRm5GUGVZQm9Na1EzYzRmSnlqWG15U2tCZnMxM056WDNlQ21WZ0VyR0NkUW1MZnBCMDlJVDNGTmJYWTdoZDFWc25sZGJ1cmJNMWZ2TWgrT1pHdytvUzQzOWNjK0ZVaWorTGtBL21OUGVLeGJIbllwTURWR2FmdUQ1QkZHUElhdVVaUmNhOXhXUys4ckRTUDlYbUhXY3ZWTWZZc1pKSVdyZE9rMEVLOG9JTGVRZ0l5VVgwMzJPNUxrSlVvaGk3eE56NWdEaGlzZW5uT28zL2ppbnFqMW5uU2FnT1hrY3hJMHhLTmpDaHhmUjM2KzNFNlhCK3VhR0Y0cGcweWdRNS9odWZTcVJYM0FFSWJrVUp0Vnh3ZWZtSFpSb0kyajB3K2ZYUVplK1Fha0RTQjd4aENacldMcExFQXFvVDc2dFNTdVlKMWp1OTNJZElYTUhFRXdhZUhwNnZmWVFDWFQ3YWsrZTFGRWd0YW1xbkFGYnJmQml4a0lQVlYzTStTYzRzYm1ET1k0dTRlRXE5VGlXR2kxMjQvR0lMTHFKTXQvY2NYNm5hZ2VybXlTRTNwajJVemdFUWJXeXJUTERubVY2STZ1Q2tUTWJhTlhuVDVqdWswanVESVhZcW92SEprdlpDK2ZGRW0rVGRQSGtXNUU1bmh6T2cxa1MwZFg0STgvZGtHUHRWMnk2aGZqd0xicHBRVW5ZbUk1cDNYSFhEOWMxem1hVzJ4dkw2WW55NU1JZmljVmdTYXdZRE4zUGM3WjZWMUpsQlRLQ3BiWVJSdXNqSXMwazVSWmdzVEtqcktmdjJLSVV4Z0NPZjB4UFhaVkhoVktDUWE0dkNLL1RUdU41OCs5ZTc0OUd3YnFHRmdkMnNwdzBvVms1WlVmZmphSzdaUEhKVUVCcWxsRFdXaXQ1eHA5RFlEWVFWVXkybHVnYzRSQkxpak1OemVYRmR0dkdTUTFJVmRZOTk0RVFROWZVVjUvSFVXL01XTXplMkRCY05xZmlPRGVBSHJyUWF3RnJoTVZrUmxQdzFWd29NTGRUcFkxWmNPU1o3bW9kRVorOWlETU5kK3FPMEZjcGV2MXdZMW1MSVZsTDl5THE4M1k2bktIN0E4ZUFOT3p3WFVGeUVPa2laeWJkUEl3amk1ZFBTTFFkS016SVUrL3RJdkxDak5LNmh3eDVheDN6Wm9Ba3pndEVXYThQVmlyWXpvUkJXRmEvcDVOSzd0MVpIRVRseUlWMkh6SUh3YWZPckxXbFVsQjVpSHVEUVJ1czMxZG5hWnFJd2hGS21HRUdpSW1iOU9rYndSTWMxeHl6b0RHZEtseFQ5dEorU2YwSnAyUS9TUSt0aDJXN05oUGdJWmkvY0VCNXRERm11QytkZExGd3FIUFJPOHV4YUhnb3VsWmxzMGZZRWhHelhmaVZyWWpIWHJwdjZIYWU2Y08rOEh4THpVaWo2anZFUW9DNmxrZE95eXA1N0ZtQlB1aU9Ib3BIbXhIeXQyR0k2UmNRWmJQejF1LzcyalVjQ3hMRUxhZGJPZ3J0cnNuaStGb281VzlOWGFYUjdIYlBDZG1qdlFqVXlvbkNsOHhFcVhLUWZYZHpkTFMyRDFQSkxoSTgvclRoUzMyVlZWY0s4V1Yrb3R0THJqbUU5ektBUUd2aEI0YjVsVko1QU9NbUIxMnhLR0s3TlBFRDVGdVI0V0V2a2Y3RmJEaktxSjRLZXRRWG9KMkllUW9SVG5STjZFRjB4bWFhbEhKc1p5dUhtcjZpSW14b1l6ZktnbHdOVVNlc0FHUm5KdTNTNVhXb1R4UFpJSVRkdGRZOHp0cGQ3Q3FjUFNQQkE0anE3a3d2di9JYUhEY1Vkb3pqMnFQbzl4TDZ0bnE4emVwRDJVR1R2K2R1U0ZBOHRURGFBQTZQaUpLYzhaNDBFdVNBd2RONkFvanlzY0RFdUs4NjVTbHk0ZlFXLzFGamlOYUR1OCswZE0zKzVlaXpsTFRINjV0cmgwUFlNa2xDWGJTT09remJtN2xtU3NOQkp1cm1zb0dlUVRUQ3llMGZnYmN3T2JFU0pIb1dIZ0FRK2lRZy9BSE0wMi81eFRBS09ldFJMcmpRa1JZZE1HY2F6TXVpem0zVDhralpMT3NFMFlpVEZDcC81cU02SURxRVNzREwrTzJkZGJ2SW1JNTRuWXdncjJVQzhJTFBPejFKaVdkY0Z4L1BSNXpVTjFOL3U0S2N5S3lvMFcrV2duVFZxZFdWK2FmRWFYSG9ZZnVvYXJsS1ByMXRTT2tVU280cWJWRDNyRVVPVVFPaS9aaEZoOHlFWnZwL2xRbk02MWExZzdnR2R2ZjhaZWlDeXdpZjRDbXJVVjMyc2ZTNmhIN0d3ZGVWckVTTHJ4ZGIrREdZWG1aQXBuVjdjVEgyMXZadTN1cFRJaXAzcG1ZZGRsbE5MZVg1VGFzRU1LUGk5M2c3Slk0V2tLMktmTmJHU0tSb2ZiZktuMkNsMmo0aHFHOVg2OWFiaTJBWHhMc3IwUnIyL1d3Rk55SVZKTXRyM1FQUEJwK2tJTWpBZmdVbVdWSkFFUkMzUHVOOW81YlRoWVllYUtKV1htWXhZK1hQV09rdnoxaGNvUGVOTFJZdjhtMUtKbk1keEZoeG41VDZrbXlsbG83T1R2dnZuVXZEOFUvekFWYXcwNng0QTllVUJ1S3JpR3JiNSt6amxtMjNKSWFUOHc5dDM1bkozaWMvdEd6c2tsNVJaOTEzVEs1QTh2cFJkU1hEKzFpMWVJbFIrWVdZNEZ2SU1VcTBRTnNvTlZLZWx0Uks1NGlVK2g5dVR0SytkSVFxejFuR285dVBaOGFFbUVZMERYRkUzaWx0WjFqdDhVQWYwUTM4akE3VHRYKzZndFF1QyswbFdPZ2tnc3pGNnd0TWEzMit4Z0xPajhsZFRhaFRsenk0ZmxwSHR2dUlubGZxd2JkbVRlTGNqTFdPTThtUFV3WTBrdEpjcTlUaFBrVW1ZelVXb1FiWWdoTEllZlBOVmFYNWdtNWNMQzFFUWVKWWMxV1V1aGN6emVxTlZoeGduNUZuM2QvK0xmUk8zaEF3S1dHUDBpN0JnNVJERFRNVUN5bVJGUU1SQ0JqNEZEUGl0WFdrL2F5TlA0eFhuNXNPU3dMZ2VqTFJuZHNuVGVHNU5tdGtxR3k2OHBHREZyWnN1Ym8rcnBEdmkvdUVMa1lkSHFwZE9HOFR0UmJVZm1VVnRqVkZhUnBvZmRBS2xyWDhJRGhSWkRmdTdSTzZXVnN3WkJrQ2wraTFNbFRoRkYwNnJERU1wemZoU1BGQkFnNGY0d0svV3dKVVlTQ3kzRTZySFVMQTlNVFdUN2pHcUloYVViV1N4azdqTnpQdEE0a3dPbVZKNXIyUktWbVF5REUycnJGWVUrbVNKMHE3WE5uUWJVZTE4RDhzcWdzUkxaSmtXSWo2NUhzaXAyMkM2ZlR3OEtZTWZwK3pobVYzU2RoTmo5ZzU2bWFoVUo2eG5TZElXVys1ZjRlaWxyOGttMjNyMWhoeDVaY05FdnFCdXYyaUp2dWtBdDV2Wi9SMitLNSsrS0k3Um1SNzMwYlNVMW82bkpTd3VHSkVFK0s3dDZ0SWtMcVdnb1VkSjVIL0NKQ242Z0JwN1NwTVlpaGduNnBOUHRwb0JGNUZyV2pLV0ZkVFpDWk11M3oyWXFMcFA0Z1RrZ0U1MkFiTmsvY2doV3pHbXVsSmlsNjNHT0ZlYUJnY1dBZ09ma05ENnp0R0p6Vm9ONS9EcTNvc0pyZXNjR0V0ajZQckNHOWFEcUZMenFocjNIWWxzWURoVGpkbkR4THR0RkxjckhMM2xGNDFWNEZnMFF6QTY1aExGbGtrSmlXRVdyM3VUNjFSVE5ja1E4ME9sbUFVT2JnbXVUVmNpd3gzVWllN3RpVVQ3RFVQRjFPejlIbmUzYS9QYXB2RU9BdGltREFxS3V4RUhNaGhnZDJsWStFTzBEKzh1TEE4UzVHOHVNT0Z2WXZtUnRaVWNKRHFVU2tpWHRBUGpZYUxHaWorc2VlT2MyNjVaMDE2U096ekdXa0F3anJjMjkzMjgxRzExZHlFbEkxSXk4K21vbExsT0thVUR3SDNuSlFwakJlMDhST1FMeEQzSXZ4SnIzQkYxY1VoK0ZraE55MUtNQkpDR0ZvclJRUjlwRFFPalp4NlJ0WXd4dW9RQWcyVVlzQzhrTUs4bndEK240Ny8xbVJTWFNzUWF6MzJyZHdSM1N0UXlpWWdRRm5TNEZrSjIyclhRUlZ3Q09EejJ2djhsbG5XMGo1MWNzKzYvMjNydHBYUkZhN0M0UWlEWktmU3Y3cENDbEtkK043NlBxNC8wNjdCcU5UTFBaS3MwSFNLblJKbTlXZEIvSS9KQVQyRlZSMXMxckJYV0Fyc2JVYWtld28rZkxMOEZKRTBSTUprNk1FQ1FFY0poQ2NiWnhGS084cktvMTFXcmtsQlBYYjdMc1oyaEdZZHhtOWJGdzd1Y2JoMDhHVFJucXMrWDlSMUJOcE96SDhRc2piTVVZenhpUXduS29VOW44dlFnSHZ4M3ltWDVMWlBQaE5hcUl5cXNRUkdRaldZem1DKzh4M3A4SVFpQzZwWU90N3czUFFhMFNSakl4Rkwrb08vWXRkWWhwMVBZL05IRVlER3AzRlpUQUNFclFVV3FiRHlPc1dxQjFqRFVreFBqOWZzRi83a3owenNxRkx2QUEyYURYTU9DZ0lDc3BidUVQRDZSZDR1K0xLVHdiYzMvTHNmL1RHdFd3dUp6Q1JBVnJNWkx4WUFrbnBTem9OMHEwTEc1MHQzdGc1Qlk5RXNYbGVqdWhhYzkvZjh1b2xzV2JyRDZENlE0WlNUSVF0RFhnQmZLVjJlaXNhSGZMNXNvbjVNZFVyOGxmYW5RWS94dmQwS2pMVmttNlE1N0NBb0Z6UHNQYWJuUGlGMWFwQ2FzY2xDc3hMR3BrTGpwazdreGY1ZGpsbWF2L3l2WHY1NlVsQ2xTZE5yQk84VzdNbTRLcGlPM0ZQV1k0M3RvbFJlMkgxbHJ3RkwyNTFNZGRwMnI5Qzd0dk1vYWF2Mnc1dG5mSkVOUVgyUkI4V1c0UHkzcXNHMWF2SVlTNWZadW9tN05RTkM1b0p5ODFWODVBWGRvblkvSmkrdGZCMHBOY1lxNytGNVBvV0h1TzA1Nnpnc080QVR0R0Ryejh1NTJhdkd0ek14VS91QUpPTFlnQWNkdTFjNVkxQU4ySEJoU3VvWmRBT25RQ1NoZnJvK01zOU52Q2xPMmxCZXFBYzB5VUwwUUNxdFV1ZHVGWU9iTmZnekpFVmV1M2UvN0Z6cXBERStmZXluV1ZONEdQUWwyVEJLWVRDa1ZIVG55VFNGdjVYODgzaVdTdjU0NjIyN1d4MlJoSUo0SVNVc2ZkdFFmcGRwa09uWU5pa3VFTlFvZWhZZ2ZCN3BlRGpQZVE0R2pZNS93YnUwamZEZ2x1QllHQUl3V1FkVHRLRTVWYzlqVTZyZnB5SHU4cGlJWE1zSmx4QkNQSlBURk15Y255VFFrV3g2b3JSZGREcXRrUW1zSHY2em1rTnRxbTNncExhM2JxOTlJUlptbXZCSE9CeXBRdVJhUi9ac2FDWkdkeWluUEVQSlNyanFTMnV4cHFWOC8vR0xwUy9LWG1IMVBUNDFoN3ZRYzRLRTFkZnZxeWg4endrRWtDUXAvakxCTFpadUpCZnNrSmlBdXpMVzMzMUV6UVFDNG05U2NPTFRWRU44WVdOeVFHZ3RPL29zSktpdm8yVEtBMS9DS3lRMW5KY3hCbTFSRjFMOHlScmhSVWdweVU4cktwYUFZc1JGK3p3WExscXJrMmRpMjk4ZHhkL0JWWEdLSG9CTHZQallTSWZBQ3BuV2RyOGppQnBPRVZFdVpEdzl3cm5rR1NhNnRWaXdKRk42YWFCZTFQeWhlSURkY2VRTEhremJ2WENlZkttVVFJWm11QXVHZXJhTFZwMzNQa0RodGtZZllMYjZkR2k0ZWlTNjZFWEQ4WHQ3YlQ4OGRUVEEyTUhMOXVRS29OQ2cxeTVKZjB6dVcxbkh1ZVdEalRJRld6SndUTUpGeHlkZlprRUwwY2ptUW8zZFhvM3owVGFMZitBZ2U4OVJ2eDJ2RC9MRFlhWVljQ24wSmN1amZMaW9tVjZQQTF4OHhIbk40RDFoeERKcFMyZjBKb0xETS9kUXpCcUlYWm9mcEVJWERWTndGOFRocm5oNmh3MG02TWZjTS9hekphRXJpOXM2RXhkQ2UvK1ZmUkxFWml6V3FveHlTcTlGNlpVQm1aNVdQTWszVmNtQk9OM1BFTE1SV0hKZ1lTM2ZsK1A3S2lmV1Q3Q2N3ZU90NHNJYUMrQjRPSDJlZzdtV29XY2FFMVdmQ2owTEw1N0p4b3F3OWVhdXJMeVE0WkQrMEVNekEwSmxtVjhFYkFkaUVKUXBmZkVoMDlEWWlTSkpLSm1OdTRMZThJTnNCQjg2NFZjQnJ0UnVVVjByNC9qMXZ4Qkt5TXY3Y25GdmhjbExCZGtjSjVjL0VhbVc5RVlHaHhCVjVYdTVmN0w2NnZ2RlFISko0QkNZZnNTMFhRb0FxQnpKbzZWSEJhWWtCMWZtcFVtdjRuMFZvdGFHelU0VDNEcWd1T0pGTCs4dzNiWm5VUzViYm1IQVYyOVFJNmZxaU9GditwZlY5R1hpdlArK2t2U2UwZHozd29DbUJ5YytUVUdTWmV3YTV4WE1tWXJGbHFkS0p2eWRnZmNVcnEya0M5NTZIRWthdjJMeGhsSnd6LzF1bnhWR1MvakVBZjhnZC96bVc0d0ZJMHNodEw3S040QmZHblJGRDBNRVkwUnc5eEkwVTh5QW9CcWdVdjlrT3IwbUt5V2dvK0kvdlhQQ1FNNjVDUFFJNGlKdENvdFNtK2JzcG94ZjI4SnNvMHhvS1B1L2pOK043ZWhJVWZBNUhvaUZKY210c09yNm85MXFaNllvTzV5NTkydGU2QjhOUS9zOU1MeHljK1VXVUxxcGlYeXBKR2NzbnU2OVBxdlRyVHJoUEZiUFRFTHFQNTh0clNzSUozQlpwMTJab0piWGJSaFlSalh2Y2YyeEJYMXB0RGlQbUZZcE1OMTcxenE5RW0yc1pZVGpjdnlYcHdrMndVWmZEdTZLVysvd1A5ekJJK01PVTZxS25ZUGxtRmIxYmJhcHUvb0dvU29XQkRhcFYzbVZodW81VUt4dUFLZFo5WmhONUJmL0NRS2lzNjZVQXl2MlFsaHFzYXNnTGhhcmFUcCs0SG9vcjlWdW90WlFZd09UWUZjd2hRT0d3ejBLcTFrd3BhUloxMXRRYzRqQW8yN2piem9aQStBcE5LVS9vM0dFYmZ2WVpOeWdLRTlUTnp3WW5TLyswekxObkxPN3kvZUE3UzhEdjRTcWgyeVlvTnRiTFY5d0w3clBjcXFCRFpXREE2R3ZmdmtkS2UrWDJSUlNwanN1VFkrUUJvWi9LNzd5cG9pVkFlK1F4YkEvTzhqVE9waWY1SXdRd2xnUktEZVVmK2MrRlpFQlcrbnlkZ2dYck5pYVNhV0QyblhGcThFTTl4UnVnbWRMdk1LOXhDOXBQcW9pUjJPMnUvVWJheVZvMklmZFYvT2RtdG0rQnNPUS96TUMrVVkxN0lzOHFRZUdKK3g0aEQ1UHRpVXFOYzR3SFZJZEREd0VUUjQ0SXdmeFBsaTc0bFZuMSt0aThXTlN1TWx6Y09Hb2xkZFpWRVJZZ3NSdjJ2NncxRzVpWFZlQTVMZnpZdE8weDhkbUl6c0FHSURGNmJGQ0RXMFhnNUZQcEVJZkNqL0xOeTg2VG83WGRreDdmSzFmYkxpaGEzeEI5cmVHMHg2SW84VUEvRGJKUlhiaTVvbUVyVnVCZ01IY0RCVmpqTDFQTUNmdnBYNWhZaDRLTFNQd2dkdU5Ya3BNdEZDMitzdmFXZlZjckN1TUJobnM2NEFZTk1kTTY0dVhVa1QwRFd1aXMzMHQ1Z3BURCtDVkxCS1R6a29oZXFNaEVtUWxnV3V1THFDcktxenZmc0dyTzhDMkZiTHhoby82aWxhc0RMQkdpTi9vU0lkRWplSE51cGhrVE41TGN3UWk5enh0YjlvdVo4L2VJSkNob0VVdjRWUXYydHc4eDcrNXNJS0UxT2pYbGF5dlZEMENuWmxQdStSQ1hpZ040dEg3YTZPSmkxdGROTFNWLzM4SVA1TGtTdDVwdXA5bENWcnZURTVLS09kSU1xQmNnUVc2SU1MVkJ3LzBoekZKc1IwUnF3bUxhVGpOS3daRjcvK0Z6SlYrWTA1SkQ4dExWakJIODJTLzF1cW9EbXJ1N25rRDdZTXEyNVNzNG51NEE4cXpFeWMrdkVMblJpT1FXcXlyK2N6ZjFUY3BBQk5KRWlIcHpZTzNRcTR5MzhqN1lXUUdGYmNzZXhBemlnVlp5MjZsN3JraEhYN2Y3UUJTdXpEbnJxMHRqV1NrUXFzTlZ6VHJFMHNxRG5RcjFqNVFTbkgrVWZrZkZXMCswMGRKei9OWm0wUTBKbnFhTU92N2ZpRmNuVHExa1ZtOC9pb3oxeEVnbFdoYlY3NHJYU0NWTUUxcTRJMWZUMEFlQVQrRTFkRFRmNCs4Z2x2cUx5emxEZldqeGFtRWs2dkRIa0pEYVNsa1RhY2E5dzA1dWpMb1NjWmsvZXZNQ2hoSW52WFhZblcyYUhuR1lFUzk0bjZjSnIwWU9Xa2JFbkZzZ1JERVRBVk1oVEVUamJPdURmbW56NGcxUXkzMlZra1h5OWtGODIvOTRLVTdtazBuS3BxaVlRaFBRTms2K0JXbDFjYTNNUUU3eStBazg2WTNzdWxPTWRHd214djFuZzZiYWlYQm12UWJWZ2wxZFJFdDZDZXYrREYxVk5TelU1NTZlWjBxRFBta1M1MzZCUk5IekxIcFFaVEZLTzJ6eDd3Q1pOZzB5NXZBV1IxRDE0WjNCa3hld0Jlc0Rjbm1tMCtLK3hlV2NNTEI3N25oVlYyamd0anNIYzcxMWNheHBldTQ3ZUs4U2VsSitNWTFOZUpFZCtLMjY2RkVOWXZHVCtjQXR3WUZsMXAvRUhubDI0YXk0dXRma1VjcG5oZk9kalA3U0V1RUhKaHRzSkIwTVhaaHRKVklrMXcwVmg0NWtnelVQeG9MaU5BZXh0cUJsTnI5d1F4cWxiNENOWkVuRW9Zc2pWenNGaTBxTVFXQU9zd3VUTXFjTkwwSHM4dVRFb3Z2cmp3emtueCt0TUdVeldvMUZ5cW1vL3RRTVYrWHBSbzVmNHhNZXpTbzc1cHhkZHVZTE9pYU90THlvdk1LN3RrSHZxU3RGbFdqYWhycTFXS2ZYeElwUVV3RlJ1UXpwYjRqK1lXM09OK2MxUEY5YkpaK1pER1VsZklXaklvdUYxRVhPbmRaY2RYbmtDcDRwS25YTFBVR0hldjJ2QiswR1dsRHREUnl3a29DT0Fxc3R1SE5SY3VyRHROMWt5ZlJxV0JJRHFFNTBCVUdBY3p6VVRBTjBKcmtkckdoUmJvTzZrQTFhM1lQbVVTaXlIck9ndERkcnpabmY5cFR4UFpXVnZrYlFDWC9vbjRYU3dreHBNaVBCeWxjTmpYME5MRXJuQkpEakRZK0F6amJoRSs3ekJ0MGFqeTQxN3RFVi8rRG5VdnZNY0NFVnlQeEcrTHdVdGJ1a0FPeFZhU04xVGU5VjFicHNZU2NlNTZVN0o3WkRWdUZIeTZaTzZFVE5zZVRPbU45VmZoYXNaQ0hITmovQ1NLQmlESTZ2T2Rsc0Fkd3ZQZEFtSzFPcGJSWnBVeHZuVzB4TEFwbHYrZjFPaVFXUTE0TzZJcGRkRFdIb2Y0cklaUnBvS2JrbGswVFo3b3dCNU1sMW9JOEZWbUdVRkJWY0txWFA3bms4TnZjWEUrRGxZdzkzeDBvaFlmQWJaaCs2WEh6cnpvNWpGR1MvK2g3cCtteDhSTnk2QWQrckFWYjk4WFk3ejBhRFRSNy9zZGk0dThROXJyTmRjdzhIdS8zcmpzM2FDVkQ5VEFnUlllWldaeEZoSnNzQ2Zob1hsZjVnL1kxbGNvUlRkSExNdk1uMWhiZWN4cFF0RUI4c0dWclFVdDkvQU02Y2RZN21LUVZ1OHIwL0E1QXhTTkpQbDF0a0Q3M3ZPbGI5aWJ2V1hxL05kQUwvTlNIQjZHTVZOL3VuNnhqcy93dXczTkpTS0ZJaVNsTEFKQ2NVTUxYa25aVG5rc1ZnU3dmOHhVbFZhSjdMUVFNZzU4d3c1b1NMYVRwOTV0RU9iTUpPejBObFVWV3hONlJNSHZHZWlqUlZiWnpXdVlXQUFnbk93eElyVW5QWGFSR3VaVUMrZElGV0ovRXhUeS9pSXdYWEp2RnhxcmVkc3pDd0EzN295ZHJYTFEwUUJVR2hLSk9DVGRweXJnL0NmOWVNS0hmbExSdHRSVEJQUkhtS0hvMW5HRDBncUlqQ0d4UmN3N0FzbVhWUHNKejdxWTFJSklIa1E3VXlETmZjaGF5ZGl1VGR0REo2SGxzTjFiUzh1STZlUGkyMVNTU216TGh4NXcyc0kyeHFsM2QvdEd2Wjc4b0Y4eDRleWU3OHZGQnlnSlhJOCt6SHhMZllaVHk2VmRoTU1lV3RpZllKNWpVYkVpbmZiL2d4WVE5aVlEVjRpTmhEL1FEa3Y1M0FtTnN4bGN2OVRpNXg1S2lXU1owd1crdys1WnlTWmdtNkY4OGtMdzBnNEhqckhFM0NLSEtYc3l1eWpoVEVlVjZQMFRPM1owM1c0MWk0cG1QdHJtK3Z3azRkMm1hZ1VlUktoVXJHNlFaMUc4c2tPZmw2b1NxMzFQYlArSjVCeFd5alI0WUs5cGpYUVZaazBvNW0wQmhjMW1pVEZaQ1A5Ti9OWFd5VTdjN1ArVnZBb1NVWDZCYjJaVmlwNzJMNW1SZ3dteXpqcitnSFVJZDRtcUNLRURTSkdYM0RacXkxSGtWeWoyZ2JOWmRYSHZUUHgwQmw5c3R2OWNmNjFBalN1TkRNRCtnRDFaRUowRXErMDRsa2d5TGE1akR0Z1FZUjdMalVmbE0zRW9KWGxKWW1TWmpGL0s4RHk0cUhIeGxwYit1RmhNQVkySlRXTEo5a0l5YjI4VHA1aFZDRndCWmdIOU1JVS9qdEkwTE5HVjZlY2FmMVJHMElPa0c0RDh3b1NOYXVRVkJnQ1dKc1JDS1kyN2poRjlIUGV4clArQktkeDBCMXhaSXJlUkNWYTU1aXlFWDN3TTYxQ1hqVHRtRHRnQ3k1ZU1QRjQ4MTdpRTdCdmJzRlBqOS9wRGZ6OU11VVllcWZVSFA5aHBQNmZPZExlaXBVUE5SLzEwUHFCRWp3T0J6YmJHUzBVYzc2QUhnVHl2NnMzcEJLL0VaWWNRY2daRXZnQUVIYW1OZnY4blBDVUR1QmppakJ3citSbGtTU1BOMnI4bWJzcUlaWXdPR2g0bDUrNkJyVXFid1hRSzZMZGxyMmpaNXhTaEUyUWRKYTIzUFI2SHp5a1IwOHZPWG5aazhjZUFRL0FuNFNFQzFYdDRmRnArSS96WDR5dzNGb01BOGljVlFVSkdnaW54Wmd4RGdTR0VrNTVzeTZ6SWJVNVpOcGdzMzdaR0lLOHg5RTZlREtBU2xvY3RXeW5teEpReWpTZUhqRFJYRTViQS8ycUhRV1hRZy9LLzhzQVdudTgrUUNuclNFckQxVWJOZmJTWUdVTU1XT2c2UTdEYUJWVFNVOGZXVVdpS0wrNmpESms4NW5QZHFqZXVkWmFQcHhXcnhQUStQSWs5Uno5bjNkd3pFWHJnekZYbm4ybFpiODhkM0JtVHB4NDBOM1lRQS8ySDY5WTJpRmZXMm5pMmd2OXk4MjJkSTdFQnN1bHFCL0JQbnVoWFUzV0JITFowOS9QRTV4Nmc4YTEveXBockVGeGYwYklmeFYzd01xbmx0QzhFM3hRaFdzUFRLVUdOSFNnVzNIQnVtKzM3K1JiOEo1Sjl0Y3l6QkpkMFBYNlpuQ01iSDFMMkFsMitZbC8wZDY4RFFGRzlvZE9GS2MxZnJVcGJ1YVBEZTZ0Zy93OWVabjFsNjZ3NWE3NzhQVDl4VjRSR0U5VzlWRE5scEtwbFdyRnQvc3JBbVhnL3pkUXc4c1oxV0I0TWpJUC9jL1h5Y0lXVnZvV2RSMmtCZDBmNFB5K0VjRTRuaHY1c1hLQ05oZnNNcGo5Z1poQlNxUm1rSHhPZHVIWHR3WkxRcHhTZlAweE82SWRYbFpBRmRjOFRHOGxzRDJsSnQ3L1FZS0o4VW5lcFM1Ykh0aGVhY0hZaGtPdjROV1RsUDNlSGt2a1dBWTh6MFpuTUZtWGw2ZHROTWJ4ZnFudzdhYWFjOUc4SVl1OE82U3dPdzlSMVpDTXZOT1NJenFXVFo0Vm1jTzlsbUttdGJyRVlxWE9QSERneXJCUm1GK2RnN00vY2NObTZpL3RKTldQQ2hXZTBmY2ErWTRLRzlqbXZWQWNJazVXUEF2MVlLamo1RjVLYU1vUU12WXNnMUZwa2FTNGxjY24wSVJuSmkyUE4rdUhCeElVQUh4OFFOV25HT1ViakN4TXYrUmlaTjZRMm1PZDNrNjJJVHBBb2pQM1dNcVZHVlV4WEE5c24vd0RheFdjZmdKaWVJS1h3TUROMUNLOEFJSzlQM3lBbE9XQXQwVm5iaGZza0xKc3A1WnVCZnZRV3N2QU1wTDVOeXZTa3l3MzBGUUpuZ0ZYTlpPbHN2ZEt5TkVxUTQ5SHNOOXZOUC9GcFFYclc1RGlQMmMwUGgyVmpHNk9UaTFwQTl1bEZNbnI4M0U3QlY3Qkl3SEUrTjg3NTNlWWZsN0UwV0pzOGRLUlBIMmxWUkVDSWI3VW41amh4SE91Vzh2anhoTmwrTDBHZW1IVjc4Z3pRb2RKMU9Ua3pVUjRQclpRYU95cFpEamo1emk0V0N2bWtnaUtVYURjWlpIUEVYaEF5K3NqbFRISXpUOUtOck5acEhRMFNEektmZHVweWViakQ0VTRFaGpSQzRvcGk1V2xWRHVGSko5SEdiWE9yMW0ybENoQWMzUjZ0TWVDajc1VmlSZWRyUWFQNHErL0o2Zm1KdjBKUmRCZW9mSFg5TDh1WFphS1VNNnJIeGlIaEtONFI3T2s2NUFhSXRka0FOQ3ZaNWZCQ2k4azg5TUs2ejl4MStLQWZwR3gycjVhTEVTdlRPTk9hYmllZU1vRzNsdWdXWFhpRFRvVHVseWhzanYyRDBqWUZta29RcFJDK0VnaEJSQ3VoK0xValF0M0RNSVFYeTIvQ1pROTlsbnE2N1cwSUw2bXordVhDcDFYTXgzWGsyalB0WEtMaGdJcUkrWERabVJ0S2hmUmxKb1hJZStUTlJNZUdldHA1dHMzL1hVL2pFbENwQ0hDdmdkSURybE9EYmZTZ0ZLNDhSa0ZtS055NXJZaStORzdPanU2cHV2elZiN3M4Y3NUaEdqU2Nkb3dDd0lIZVM4Z1Fra1BabThvWWlDclk0T3pEb0hYUEVDZC83RnNUdXVybGorWTRZQjhpYUQvejNYWnhZMnQ4MmtiV3pTTkt6WlhZcWZnVDlIeUM0di8zMFl0V2lVVnI5L29uSlE1R3NkNEJBOFdkaDJrTFhHSnhzdXE0aHdsZWN0WmlzR2FCNGttVW1zTHd2WlRZaFZqTUdHZkFlNTBSNEs5Q005ZzBSVk9Cb25oaktsVWRHdHBHOEhhR0lENGFhY2VNUXhUZUZTR28za3pBUDY3RmJzYS9BeFJvb3dnd0dQY2cwMjlCSmNBc2ZlQ3psZ2tCK1NRN1VOemU3Qm43czJKbHk3Y2srRk0yS1JNRmMrdDFnMWZ3RzVFcVZ6ZldFQ1BMWWNXODQ2TVdWbzZQY1E2b0JMNEVvNGd1cW9WaDE2K05hb3k5c3lBNTNIdnNXbGhpQ2ZobExzOTBYeU1RMThhTE92azhLeHJDR215Y1ZLRXlSVWs1WC9HYWtyYVRWdXJTNzh0a2FsT21peTBWSC9QY1dHVHRYK0xBd2s3V0w4R2YweEx3c2tJTTlpWnB3Y2JUSjlNR3dPTG91Z3d3NFArRTNVNXllRGVrMGJIM2hnbjNvVjNraStTbW5Vait3OXNJRjBQQXkydjVteEVKQnNMajNtYXZ5dzdoeWFBOGZvVmFnZCtaQ1ZpbG1KRXArSEJwL2V3TlpTZnhRUFdvWERWRFpodXhVV1J3WEdXRWVWeU5HbTZiVlAzVXBPdk0wdm1IUjFqMkVVSW9UU1poNmZYS1plbXJkalNZcGFPQlV1Um8yQ2JVQ0lEMmErS0x1c0hVU2k4MDNxRUNtQkNxZ0F5aVFZeERzL0lXdk5qTStMV2h1a0ozeFY1bkZ6eEJqdEo4QWdteVpUYzl0QXB0Y3JHcTdJcTVQMzU1OEJIL1BuSzgwdkE1TUdET296dUttV1BaZTMrSWhodXZtbjVjSmZ3cnU4dkVDYkNLTURVamc2ZWRqWUFTdDRpUmJiV3JoNG1JK1cycm1pWW8ySWxnaEZqKytKMXZyMHZxZ2g0ZDlGMzQ5ZW9zdjNuZUxBTWR0UWhma1NhMmt3Y3Qwc0IyVUdzNThDdXIyQXN1aEFQSzRpMWlnTTN5Sys4Z3ZiSUFYbUJPSU1nZXdhRUlJczR5RytKMC9zQ1ZPbTlyU1RaTlhEUlN0RzhjbUNReWQ0c2pKaXlVSU4rVVBCUXdRc2RoMVcrNm8yZGRVZ0VoNHJXd0t4WStYQWo3NFp1d21mNXhTZ3c4SzlESWtwN3RRQjlRYi8zNU4ybmFxQWVkQTc2VWE2S1phdEU2WmJzNEZZc2VTc2U0bG5XWjgzclhoKzNkWFVUeE5VeE1vaFUrOUxtUTdCbmNpMnd2Vm9JOVpkODUyT2FIcEEvc0RVenVhbDFZTGl0SVdHSmo0bVFQUEptSGhEY3UzKzdTTkFRTFFSTUJEYS82dkRjbk1oUWtlWk81V1ptS0tubm5IYlVoMTNlK2tza3FKOE1yeHpwZmNhdDBxbTRhbmZEUmR0OHkyWHU0QnoyYWNDUWlNRFl2NlJHSnJxS1VURUdiR2doWmNWVmVvRFV2ZTZVcGVZRytxWDduY00zTk1zQi8vTVFrSVVPNjVuTVNGQVBISnA3eEx6cU9XWHBqRDM4ZlRFVTlIaU8rLzhoMFFoQS9xRWtESzlGelJ0c0F2RUpOUU8zVnc0cmJQbnBpWTQ2N3Q2M2FCbmlFTVdRU0NTakQrSldMeUEvdHVrMEVSSFFLNmVNNFVjYVAzQjAzTFMvTFlncVY0dE56UlJVYUhmNFduTFdvRnJZdHBZSGQ2Y1BudXlJaXNtVWV0WU5iSjhmYURsTVk5ZmgyME92elRpT0NFSlFQNW03RWlyV0NFZnVLZi9URzZoNWhScFJ6dHJxdFloR28reFJQWHptMFFXNXZFODRtZWRHUUF1ODhYa1A3M1RnbU5DTm01ZHdnZ21Rcms0TE41eUJ1UWVyMkZqbFlZWC93SnVWc0luMUhoaHdCa3RmYTB0cENTaDBvcjE3bDVjc1pBdWJEZjNnbUlnQy9BOTg1MjNFMHFONGJaVUFud2ExNHlMLzI0Y3czemhYQ2xrdGJ0TnZGNWJqRC9SZ2Z3T2hCU3NpT2pqTjQzek5YeDYzSmFXUUFGOG5ycDI0R0xFcEdobGNkQm1VWkZwVlBUczJKNVlBYVZMSm5LMEJQTUxMdlJITkxVRURKSUdXOWhmSG0wVmVOSXR4Mnk2MTVvQkxJTEJ3NFphR0FveFBLTmc5M0lwNGlNdTJod29ReXBFQ1hDZ0RqOUYwL2Q0NVZQYURJbUpmTGIvUDVWWTBmZ2FYSkx4b2NBakJKbGNFWkJrNnFWQ213Sk1pT21PN3FQSWlVZ1lva1ZKTmpxSkxDSGlOQlFEalBiYWFCYW5nekI0UXo4WXRTT3hVc2tEZlNzaEJ0L1FaVXJ1TzFnVTI0MmZqU1hzVU5jUm9DVDllRVJGV3pWQzZGQmFPbUNFNVZNWjJZY2pSMDYyOGRMMlMwbUxaZ3dDSlRwZGd4K2NrRDhaWFVsNVF0QXVyS3FBUzN5WmpjK290cTVCRytsS1o2elZkcmhIUHBNNFZLVGNiaTB2aXdOci9tL0RaNmhhNzNqZ0FweTkwRWM0d3dtNi8wajlTcE1JbVVNVjc1WitXNVdKaU44ZzhSS1AyenNMdzM1aVFRaURIUlNRV2k3NmF1R2k0SnpLT21YODM2Sm1NK0l4d0gyMU9CalZXWVh3MHNYOWhlMnByWFZ4SGxPdUU2Z1YvRHRSamgvT0V0T2lZbGJqRWk4NEpreGhUT0hQQ2JGQUcrTGxDQmJ0M2c4eWV0ZjZJaE4rYmNjZG5DeWI1MEpMQjM0dEg3OVNETjg3cHFBVk1rdSttcDBFNmc5aDRzOXhEQlU3RTkzWlA2NVFuYS8vOURva01jVVZkRm1qcFJaVkhDYzVXVU9yT0kzNXl4RzJ3OTRsRFdOYjVYRnZ6Ri91QU1YOXpNd04rLzBqVy9NektGVGdWM2hOci85dU83b0JiZDE3SS9IaHFheUNJRkVSMjc2Q0xOcHlVYXFSaHlYL09Hb1pTUDhhV2hGcVFaREUyWEdnTjBES2JiMEs4Uy9MRmp3VkpvWE5CWGtNeEJINlI4V2pYRU5CTFdFVHgrbDhGWitRSW00S280eWNRNE1LTzJJakRlT0lDWW93QVAvWHZJbGNxWTBiamlORDFzaVRCNGExSVFsTlBreXMybml1RnVrcXUyRzVMOU1aZmUxRFZ6QmllRERib1dVTTQ5cnRmNENpTGlEY1plWWdxNm13cVV4dDkyc3c4YTVSUURuUU04ckswRkRFclgvRjVGRlJxTjFCYkk4OVo3R1h4VnRpQ3p4bllYOVNUNkJPMkdoWDB5bG9QSTI1dW51NzJ1M3pZYVE4c3JpUkRuNHN3bDZVUThCekExUUR4VVIzVmdvbEUwdVBHSlNXNlR4UHd1alJISVZlZHduVlN6NzA1RkM0eDZxMzdtaFZjaXRWQ2c2QzdmeXUxZ3hTVExLTGZOQUZ0WEpBVFNsYjVRZWlrTmUrdEVxNHhNTlkxVEFOMnhZaXlJc2x3d3UxYzhoQjZiSk1zTGxtb082bFZONlRUSkRhcTdjUlBjbk9LRXVJZ05GN2grdjFlbHZDVVZHanhIRjlQUGQ5Ykp4RjNXdXhTLzVzUVlpVWNZZzZRUVBJMlZaTXlyOWZoVlI1RDEyd1RqRmtNODN0QWtWd3htaWxFYXdUOG5ZK1VCWkxvV0U2WFJqN282ZTFxYW5yVHV0aEpUcG9vQm1peHNCWlRlL0hHYUJHZVloc1h5TVh6ZmJGQTRoWEVrQmJWaGNzUUxzajNhcFJTbDczcGRmem9tYnZ1c2lzeEtwT2tWSnpOaU9LdXlrdXRxNmxpNkFRUzJVT2V4NloxL2E4RXJkRDhGWWRQMCtqS0laTU1SN1R4VHJ1VHRycGJkRlRyM1BFYXdSUDR0KzQvUVhkV3c2M1BmemljM1pKRkxTZUhMbU9vQS9Sc1R5aS9KTnZ0Sk4xMjRyMDkremQrU3ZINjFhaDRXMnV1WXA2UjcrYXkwbzgxY0JXOVVYcllUL0RSNkVHZnNIOXlhQnp1aGVvd2haM0ZkMjVRbUNRTmN4RHYwNW5BaS9OVEJXOW1qV0QxZzQ2bHlMYlpSRHNjOHpjWUJES0NCT0FkeWVnUFRWamZhUFBiQlVyUmd1VTQ2WEh1Wkc4bWRERTROVnlGclpJTFgvUmRRTzlkVzNmSkt0bXByR0JQbmx6cnZDODFaejhMVGNydFVyYlZGajJINkV1OVREUS83ZHN3RTFPMXgwdTc3dnh4d1d2bXYvVUlzUjI5NDYvZUs4czhiZmExQXdzcEp2QnJ3L1NQeHJtYndvZ3B5aWt5cEJOOE9QdVRoYjl5VDVLcHFHeVVQcmUrQStrVzhQWlkrM2xBS1lCSTVTUW42byttcFRtOWphdVJDWDIyanJXekRETE1YK2p5Mm5JeW1oQjArbkNXQ3dRUHpLSFpEbHpMYUt4dEc1VS8rOW5GV3I1S2tmRStrK3dGRllFNytSajNkUm1wY29BTmsyR3ZXSDM5VnpaUVUzdUZIYjIyeEtaN2hyMUxVVmtZeXdqVWtJb2Z2UjNCbjc4eWZJS0xJUTI4SWJ3V3VYQjBSenRSejR4ZmxUVzRXSlBkVC94VU1pSlpmbUw1Z0VEelZEU1Jzam50cWZEeFBXL3Z2Y282cC9NRGhGRGdIT1dnM1pVTHlwVEYwVlE4bHhwZ0thTjNWOCtwS2xWbFgxNnFCdzY0ZE44eUJYMFlmanpLUzc1MUVHeE4vdFR6OW4wb1FDTkZub29SOHRQZ1hkSG00NXBIVXJrUWxjYzZnQy9xVDJ1cU9KdjhkZkZvejQvNGxkYXo2eXlQcHEvMFFEOUpvdVFlTVEzL2NZRDdISTlkbW9mZTAxaFdkUjU4TC9jQlFyYS9qUUZvUm1Pa081SThuRzFqVjZFRnBvTDdZY2ovaUNkR3NNU1RnN0RtdlJ1YWpSRUg4YmZvWGFndGZYd3pqRk5ubkgyV0dOQ2R5eUtGZHhKTjVZUHJONU1zWWJPa0RwUTVRTmxPd2xjMitPZkJxQmRyVmp0TWp6TmFUeGJMNENCSVU3dmdoYnJQREtYKzBGVkJVWVVLSVE3VCtaSjY2K0VmajlzRmk5RGIzZmdLays2UXgzLzF1S2hDTGF5R0NoQldwRnlrRCsrVk5GdUZ2MmV0cVNtVzR5MFhXUDRZYys1eUNiY3JVa3dpL2hmdWdYMU44ck5Ld1B0RU1Oand6Y2U2Mmt1U25nR3BKVEVqT3l3TlVqWEFtajZsU2dERHh4Wi90K2lwZzNuMEV5ODE0bE1aYndqeStRS2g1Z1UyMDN4dGM3RVI1RjFvN2sxYnZPWDY4emJ0cjNBWUxxNWdiQ1JEVmMwZk1xZUE4Z0w5UW5LR0d4UCt6VlpQVDZIVjJlZXJPSjRiaEwvZkk1RTI2VXRQRi80M2NNZmNWTnVZWDBDWm9Kam53T3pGazhhU1VLeng2Tytvd0N6Z0M4WGp3dGM0cGxwai9LdE1wU1QzekpBZ01UQ1FoY3FHYm1pUkZJbi8yZFBMc2ZTR2JlcDRjZlJWeHhhSTVEaDhwckxjb0p5ZjdDYS9WMDdSYzZuOVFQek5WUmpTbTZ2NGorWWxraGhud3ZQOVpxNXdNRDNpZE5wcThtOW9ScUFBN0dSZXNsTzg5MENiQTVYT0xRUXRhc1JTV2tSREprZllQVWp2RVJtd3VmNVFYYjhUS2VheXhkR25HdkNNb1RyZnVOemcvOFlvSDVVSVAwRXFlZVRNQ2VuV3JTV2hqRTk0bm9wM3RKTE9UVWNoTjdFVjhMZ2cxMys4TDVrMU5QWXBGSHNlZ3BNUmFEdWgyaGdnRlRRQjE0QmtZTHJMWDF6VVJ4SG5zZTJ6NzFRaWtnNnhMR2JTNmF2QnVqOEY2NUhZSEhCcGlvSUJDNHpuU21hUEl4N2V3MUM3OHliaEYyK2I1Sk45dno4ZmFHU3poK0tJZDZCYnhVc3NsN3NDbm5oVTk4dThlK1BRWnhHa0tEMHdRQU5MMUVpaVhESFV6VDgwMnhCajRYQVc4dnFVSjJ1VXJrbW4wMTF6eUswbFQvUmcvMG9Ld3psS2h2bCtyeXBKVGNDM0ZFMFR1TUhDd3hKS29zcXUwaWNHTldCSVg2M3JtekRjVFhwWDB4YTgrNWh1UDNpOHhCdDBwWGVUREJ1c3Y4UnJna3l4QnhNT1F1U2JJZzVabjk2RFVHQ1FMZEhEKyt6RlVwL2VkcjhDT3JEd2NMWmpId2ExMHQxYjJvU0F0dEJ4R1VjTlBndmFPLzFZV2lMNkcxTFJSTXYzWHdwajJPdjZJRU14YUJoYlNkUFBZcnVJL05kV1VOOERmZGpzN1NvWEs4ZmdiMDZsQVZISWQvYnArTVJseGRDc3N6cCtTQ3k3MC9Famx1aVlJcHM1bzQ5bWxWWkd5M0Z2TUpsWHRzNHQ2UUFWVi9kRU0rbmVFRGlNRmtWUDlBRksydDhab1JFU1lIWHRCNW5PRmpPSXkyZll6Z0lGd3VubWtodGszbFQrdlNJRm9LVk5vSXJnTkFVL3lOUGNXd1RBM3Z1Rk5oQzlnU0dIWkVRQnYvRmJ2T3hIcllyaDhqbzQzd2xaU0c5MCtlZUFPU2l4aW9QU1preEJ3S1RFWWdoTWdhVEx0WUhWajFOR3dFNlJEeWdzMjdTQ1MzL3Z1YTNQZkFKSzYvOVFNVEdKWDM0S2Y5Z2grOG1XWEo4NElpVVhLdnlWQ05seE9sMks5MEhPMUMxM3Q5bGs2NE92cm93dG5uUUx2Wi9WVUYrUE0wRjZpVld3RW5yUm1ONURNa0puWU4vbGkvcitVWngyaElNbTVlWG5taWdiU0htSG9FS2VDUkpNbWhoaVVWMGFUNWVQT0dRSjAvbDRiTUJrSTZIbHM0YkNaclZqWmxFT28yMjRiMzhPSHUzeVgxSmlvMDFWTGExbkVrb1FhM0RzU0NyalZ2aXhvV1FxZUphTWpBUGtVa2U1bzFSbDNsazlMUHRXY3YyR0ptQkdGaks1ZnZOcTFScjRCU0xrWU5ONVNyak9GZWZDeVFpbzIzeFhpME80empoVzMrV29kUndEeVRHRmVrUnVDSFgvNS9xZTZYY3czRzZHZkVmQTkrZzBQMVlVemcxS1lIdlZvZkJVSE1WV2Q4RU42Vi9saHFnY0R2cllWQVQ2T1FwbnZTMkR6N2RKWGRXcHZIWVljMkpCSkcrdW1xV2l6LzFOWDJKQjhMa0czT2FoUHlocEpxTVRha2dBVFRxeVlLblE5ejZ6V2I4ajBvcjlyR1h0NDVsUkpZTE42WEVZUW0vMjZENHUyNGN1Uksza2UzZncxQzFVTGZLZFBzK3B1aXJuQ1JpTWNoYUY2WXk2VEFPVTd6UWlhT2wrYmwzWSt0VVU0eS9GZDZ1Rm5wWHRMa0xCU24vUTl5MTYvVitSV1k3dDN5ZzhLNlpuOW1TSWR1L0FtSEJROVRMRk1Gd0o5OWNhWDNIb21XL0YxVGlBMHB0Y015ekowN3B3VzNkQ2Zqd05DbHpITzhxM2l1UTRGclluekh1WElkT0djM2RvZDBKTmU0em05SUhpaW9WMW5UVkhvSnl2TGZYcTlVWkJwQ2NCSG5JZ2RhSE9ScG50RjZmSS8xMWpuYjhDVWQ3a0VqM25xMEx3S1dCakp5Nk5Gd1ZKTFpyREFybnRRRDgzZC9lVUdPb3BUams2Ti91S1QzSUFYSmhGM1pBSmJtZmRwZzF3MGhQOG5IRHhnczdmbWgzdXhjbzNSdHdKOWl5VVUvRVR5VklBWG1JMjFNb0RCaXJweTViR3dhZ3JKbFRsNjA5NGs4cGhKT2R0R2VjQ2JqQ3ZzL0J5bllSUlA4ZkRiUGFNZWFrWmlWZHloUncrdXpWMjB4aW9NUWVuNHZyRGgzQ1pVak9oMWV4WEtTT3N3d0ZCSk5WZnZiYzBpS09WVkpUcng4OHRwSFM3bFNjeFBWZ0pLSXJZaml5THZhZVh1YkdVdzY1UkN5MmwrYVhsWU00bDl1a0hVaVFJazhXZWdpb2pCbUtwbHpPQ05IT0lIMWYyaEJpL2lTdXU1VHROa0NaQ0N3S25wQzc5SlJ4VDRtYTIrZzVsWEJTMjkrQ01MUUxwcWpyRVlQL2tQTTM1NzE0eVZVbVU1ZWVNNEtDcWRjZ2xubHJOTkVnNnRqY1dQM1dHSHljRFFxcGNyS3BqWStmQUFUSmFEQ3BQa05mSVNPUmFNQVJsMW1XOHJVWWorNFpKOVREeUJZemNtRmdJSWowKzQ1WVJlZEc5d2d0NXluSHJBWHJmV05rVFFWdHp2akd3NE9TWVZSYWJVTE5YTU1ZQXg2RmlWVmR3dTRTYzhNbVU2ellVdW1BV1p1d0VBNFhrSXRFam0xdHpJMXU4ZHFDcVhMUlI5akQwcElkcTJ4c0R4ME9GeGpJYklnTXBEVnRndFZhRXZCQTVURWRUMUtUTmFvVDRqbG5QLysxWWhpT0xaOEx5SXJtUDR1YjNSZ0I5ZFJ6UW5MbmhmZjhGeUZVR1RMZDJ1QzlqaThncEpUQnROemFCMWxVOS9RNFBINUsxV2NUWG5PYTQ2WnorRDZ0VWc5b25nb3lTZ1h1dHlWcDRrRjl1WCtqRGkvamNjWGh6bzZVYlZVSHpmV0ZEZzlRQlJUWHFQRlhTN3QyZTlIdVAxYmE5bXVPclAySlFEZng5QVVDTVlYNGowSEtBSVJnZ3F2MTZwZ2pwbXNCV3IzYXp1K2ZXaDNsbTJ2RVN2MlVEU0x6N09QbEo0SzF3U09zY3R3YjdwNGVhVG5iWUNMaXprZC9yZ0NyejYvWUhseTV2bHF4dTB6aGJOU0lPVk9ybzBXbTVJUVNydi9IYlE1dHNzSkp6OTVkOHBCTXFzVFFkUC81WGVSZzI3WHQzaGc2dHhJc3gyZkNnalN4YkxXZjQyVms0TDFhK0VNemp2NkVURDRGcFp6TTRBclhCWnZOWHZ4bkh3MGw2NE5kQ3NodjFKYXRZaThXN3RxUFgycFFBOWw0V090OVIzZTU2MGdpazkwSWZ1Q3V6Q1p2bkhlWTlLWUorekNDSXBlNDhjU1c3QndCUDVqUXFGWkV3QnhodWJOckw5dkxhMldMY0toNU5LZ0I1WVpvKzJrUzlrTWZoazBwQmtwSFhJVE43UXlRSGtMZ3k2aDArMUwyWTV2V2tUeUtmZHM4eWlZUWJPQUtnQjZ0VjBXd1FHMWhSdzR4NWZRYitlbVdQMStZcitKVmNTTlJZY0MvUkxCcjFEdHpUK3JOSUs1bGQ3ekZXSVJpbTl1STBnbUJoSGRiSUZiRVYzTklOVTQ3RE9OT2x1YVErY1E3VXNEWUpuR2hNaDY0SWRUZVRvcndlK0NkMU84UmwxajZXeVdhaGl5V25oZEcyNnp2djNVWTllcmhDOHNMMGF5aFRkemxNMFJSTWNMVGxBWGJFRTFCVTczSGFMVW96VmZhMGIrODVsYnMyZ2VZY21uVHFPUEZvTEIvUk1RRnRCU2dmZTkrVEFLdmdhTHpTeERkb0l1V3c5a1lkMGhpRGJJVkJoNHJ6elMwdmp4ZkdSL0hpdWtuUS9vNWNHa1d0V1ByOUtvR2c0QnhhS0VYRnFmUmMxUlM1ZVFpOUs4SzFpeUl2aVNFYTJZcHVrcHg0ZFIxWkVuSUF0S29NcFMyZ3hOdExtOUl5a2l1L29oTjh0Qk5zSFlTRFg2eUNpY2JCYWxzWDBrbzdpL2V3WCtKVmREcldZSlpzc2pRSmRlTmlOWWFCMi9ROGRxK3Z2Y05nUTFKbVpqcWJGM3Z0Z0l4OFJLai81ZFRrM0JOdzJZODhRL3k3UHl4ajBDdDZHN09heThDTmhlZUV3THdSN1pqOUNtYXFQVE5leFVUb01ScFFqcVU0bExYamZ0VEErWHpFRkVXT2lVSk9yL3JpbTdKQnQ5VHFkNmVadnFhb2tnRlBFZjdWblhoNDU5eloyLytiSHBkWFNKdEdQSjRJWE5nTVFLcDdZOE9OT3kvbjNnNzI1b1NveDdaRyt2dy9FVkF6d1Nrbk5MRjk5NVk2NjlqcER0QjZVWDljWURvYlVEcFpuWXdVbFBjT05Ca0ZpMEpja0VNOVp3SThrWW83eXZndGpvQ045aXNXV2c1VEJhTWp0ay8vakVOTlFST1Mwc1VxMlNVTzF4enp0ek1kdW9FSXNqNGhMOUZzZFFSMTgwV3ZaZm5vMjhhODZqb25wWWdaVkttd2FkWlp1d2dVQm1mN0ZJRTJ3QlkwZEV3V3R5dXRvZS9oYzlLdklOaGlDZUtyQ0pjd20xOFFqNlJSUytDRXNqN01UcUlsZ2xnL1N0TE5FZ09GZHpKWlFmMzU4TlFsd1JRQmZrTk4xSlplRUZXUnYyL012amVITjd1RUdheVdKZk1UaVJyMHdZUmNOcXFRZ1R0dTBEOEtKWnZNYmI0MmVjdVRxQ3AzaVJma0pQakxpNzRlSzVTZ0h4anVnU3FPOTVLU0pFM0ZnOG8vWCtVeEhSZW96UDlGblpkQ0Y1N2l6Z0xZZUtpSmJ0c3ovVTRoRTVCK3gvaHhXeFNEaVB4d3FzWThydTdMTzM1aXRHdUxOVmFrVG5nS2VlTkY0Y3RZK21maG9lbzZUWERoOVBZcjJrQXlEL0Ntd1BkSm80NzN6eXg4MVAxS0dNMVJ5dEFLQjlNYWl5VjBkV0owdmRFMTErNTV3Y3NaMjl3eno1dWFLQTNhd2o0R0d1R1ROR1pIUGxqb3A0MG9DU1RucnViSnY0SVdzcFZiaGJmUmcrSlUvWE1OMlZQUEF2dEtvdjk5aklTY0NuRjlrUHplN29Kbm9KL3h3SVlkYnRBSWxubWtvSVptT2duTUpsR3U3Qm1NL1N1bWViRGU4VThYR1Iwc1BWdGNsckViSlVPV3pNSHJPYkFhakZtRFBVTDZTL1owMWtKNXJ6RmFLUzduVi9vV0FHSVpEd3A4SWhVTzhEQW9nVGY3S1NHZjkwUlRYYlZLMnNrOU8rdW1XS21ZQWxzVmxINGxJVW9rUzZscnAxNHZ0TW56SE1XUGFXUEF4bGx6U0xLS1EzMThLZHV6TVFDWG51WUJpa0JHYlFEaS9aTlFxeXdWaEFqTHJXRXNaMjRQRVJHZEZLdW0rNUlmNE04WHZ5bVovYzNDdzUrbTk2SStVOUNtT0FTRWdVWWpLMjA0MHN5MS9BRTJ5bjdIWkI5eU9qVFhHeURETGd6YnRLaytxN0QwMWZEcVo4aEM1dUN6OUxXWGo5Zy93WnlsOXFxcXk5UFJpMzk4ZERaSG1kMitFbHd6RzY1aE95YVFJakJEOUtOWW9lbkN0aGdkVVB2VXQyNUxKZldMK0J1djd6SzQ2VGlmandMcUVBcitiZUU4Q2NaYzNrV0xicHVrRjRIMFcwMkFGTjJlMnhTQmFCMjVwNlhtRDFLZ09xakU5WVpudnZrbTI0bHVxajFtakJjdHg0aW9mQ2tBci9TUnErbDVQb3pSUnpkM0dUUmtJQ1hEanpGZjRpMXd2VVlqNkFxVjRCbWRrVzBPWnhrclhrd1U2d1FXMm1NYlk5czRMeWRETGcwMVErajhZd1Y0N202SDNvQnRJRjhvSE15dDNMTGNyemZHUmJhblZtbStFNVBETU9pbjFuYXFrdzhCWTU5N283a3l5RmtmYm9YcmQ5bDRLbXlUTDZIQ2dtN0ZOVHJBTDVMRGMvVzk1UDQxRW5lRWxtTkp3UitrdWEzTit4LzNoK2N3U1g5c200NlU5LytsMFFseHdTSHJiYllQSFZpMXRNRDlqOXY4cStxbUozSzVrczl4WnJTQmZwQnZiWGpKT3dJSWUrVENKd3ZwSXorZFlSblphYXE0Y2JYbjB4TzNpTlAvcjBhRzUwRlJVbVYrL2xTZjM5aVZTSm9lT2RyVmpqMm5JUHR3TWdWcWdoNytFQUhBbndobE5acG54aHBOakVFdS9UR3g2M2xBVjh2V1ptRDdlcGo3bnJNSXFHaDU4OW1iT1pvOVZmQWlhZkJpVkkxSEU5NUp4Zkx5ekpPdTRXbVd0VE5zUmZWUTRvS2J2aTlSMDF2bkcyWnlDRi9LY2czdHVlZXNNNG5vcnNlTHBmdEFHY2RZZ3B1ZmhzaXdoRlVmd3BDS1hhUi9OMUoxdTJndXJGTlBxNlRrNTE5YU9Sd1ZqT3lqdldEZmhzRkcwV080QTdUUVhnazBMTUdlSjhOaWRSdzZTY0F0UDZaRjVmOXN0WCtTUnlRWTBNVVpBSytaYXAwN0I4REY4ZmE1SGJKZ3NXcm5hTjRSSGVocnRrQ2JBeEZEOTE5RHJURlpqcHZRQ1F1dEZVeEtaK3c3Y3JUUjRid01LUk4rbHZSMUpMMjRENmpUWEVqN0RBTm1zaC84dWMvTHB4MmZFcldnMFZGd0RvMzNnejR1Z3dTRUpXMGtrclFaQ0crOERFRVo3eXA3aHNwZ0Vqd290OVV3ZHd4ZW13bXR2Y3RmOE90Z0hSbmRNS0pvRmd3aUMyajQ3b1ZZcWNPSVc1V0JzaDBtL0JoN0pDanpGSWpDSWhkY1lZc3JIMWhwWHgyaFRndGZjWGFXNHFYWGd6S2pyVFVTWG0yaXlmdjlNSGdkZ21naUZSczF2TkhHTkNjQVlBaHNRQmdyL0JUYTZrSHl6SzJoY3QvZTVQc0RhZ1VaOWwvSkxleDBjVkRtUXAyM2xvNE14L2loQkNqMWxRM0RocXV3UmVEUzdPdTN1TnF5T0FQOWRudjV1OGc5dDZkMTdoeVBDQUpKczJvTk9YeG9OcVRqWDgrZndYcml1YWhJemlReGpWRnQ3WnQzY0hGTEJBelJPTXN2OTVwc1pJU1FndHMwMXpmM0NTZUlYOTQxS3dPUml4L2NXaW9mVjhrWjZXQm9PVkxsTHQwOEEwZlRuaDdZMWpQUVplTE41elZWODc3SFd0REZoQ1NlWG85Y0JyeHpkVDR0c1FZVzVDMWcrTjBsSEJhZVp2Sy9JR3BHWjI4VFZzZm1HSThVeUxzOFg1ZHNGdUlCcGlVMXJYWUxPbEV3KzNwcVF6UDhkTXZNdFRKMEV5c1dvb3c3Wk5lZXdMUks3cmVzWDhOc3NxanpWYnNOR3BMUUl3dnZrUzF2cER3MXBzbTg4TkMxdDk1REVybWJWdy80ajhKK1I5ZTYrSDNpUFZ2MXRQOENoME1DV0crenZoTlNzdit1RFhFSUNNUG83Y1d1bk5RWXZFZCtlengwcUp4cmZXNTVNa1dEbWt5YjVHZTJ0VTRxRkdoYzVpTnlWZU1kbHpRVUN1dS83ZmdwbTQ1OEhVRlZYZ2pkOFQzcmxIVGQrWkJGaVpMSExXZnVTRmlobXZOYTZ6N3FJNDNJdVZFUkRveHNReVBwbUZnTlEyQmhweldhRE5jZmhkT3NPV0ZOU1pBMEZXWkFkN2lLSm4zc0RJN042UStmR0R1Z2JjTE14MFBWQkVRbStBNmJLQjJFTStkZUlzZFl4bUd5SS9jdVQ3OFhEQUNFaXE4YTIzb3RSd2x2ZTBteWJaRzBlNDNtcFdLZDZNNjRoOUlkbzA5by9XYWJaNEpsYlFTV25IamZCeTJwY0RJTmlENDFBOUd2YVUvb2xvTWJOeHVqV0JyRjhnMm90Q1N0ZWlWWTVuSldLWGxHUHgyaTIwSHRmYjhqbDl1dG9Ia0VmdG90aUUyd1BHMUVvbVRYazBhS083ZWdrTHU5TVJ0UWdSeDFzWGErWnVZdDR1YmJtcE5sbXNyK05RM01GNEQ1cWl4UWZuU3YvdnVoV3lpYU9Sd0F3L3pvMmtaV3FkN3FMQkQzZkdpMGFGSUgxV09iZnR6aWRudjdTU0VMVnUzbVY3bUxmYlRBcHRoMFlYM0tpOERUaHozYmN0YnVPVEJ0c1ZjaDFyUlNPbysxRWs2MGFyWGhJejcyVDJXVWp4VXhNRStmbHQzdWhvY0pXKzhNZWJmT1Y3WXpiS3U2cXcvSy95S3Y0UFZaNW0vYmxLY29BbTIxdEtoSTZHcTB3ejhHbmFQTnhqaVdteU5VVVMzZ3d1VTFoRUlVc05oZ25Xa1BicUFCa0VpOHR6a2doYlFRWnZjVjE5YWk3Yy9QTlZKSHRPQnUzM3FBYUp1QnBodnVUQnpQcmdiY0NBTG90L2FtQXJMRDNtekpMdDd5UTZ4QVorSnNSQk8rZjM1OC9VV2lOUHcycFRIUlNKVERlbU5XSlUvM1lXeFBvdzVWaHpLWGVXQUtKYmVtNE1HajBYR1VycnVPcWw0VTVZK0Q2cllTcGwrU0k2bVFhNjBDcHhON0luTDI4bTBkd3VGZ1lHL25XMmtBTVpQL3FySDFDWE44Q0tGbk42YjBVbUdiQ1djSmdDZE1DQStoMktNWGQ1RGdBcjhvcHI4T0Z2OXBZSStYNFpPY2FTOUYrei8yKzJLWG9WMVV5Z0NVRFB3QjBUZW9rZTY1anMwRVlVeDVyWU50ZlEyRXdJTkVhL0QzVmlwTUg4Q0dLSDhvQ3MrdHhWVk1SU3puQWR2eWpyLzQ3SlM5cVJrbTlIM0l2ZDRkK3g2ci9ncXYyUmFHcGVCM1YxRTlLSGkxM21nb0pLSS9qRlhxWnBpelRIcTB2cmdZMkZLbTUrdVp1NE1SMFJuWW9EMmtEYXZRZURJOFFBZ2IvVjF5ZUdReHNwajRURWpMMUhwS2MzQytGWGhFTFdtL0hQUEJJZFJLNmt3QTJ6dEFGWGdTejJoeTViWVhnTEtnelV0T1BHZER6aDl1b3NFNENUWnJhcEk4dzhyNkRvMlpKMEViNmRtaFpEMlluazRtV2d0UG9Gd3hCcThBQy9wczlxNEJudEtJU2tLRnpDbmxBSVVyRzRZaHJGY0JuU3phSWtvR1YraWNqY3Zkc0hZamVUanUyREpYTHJscWlDenZOaTBPcDZiY0RJNXFPeFNjTXhJcFdldGt0b3RXMnlsQVp5OVZBZGw1R0dsQ0lxdGFiY0kwUW5aTHhYS0lGd1YwL1NxR0gxcE1XLyszQnBYWXZiM2x2U1pUc0E0bk1ZRlY5eXRRcDdHdnJxelZBV2ZQS0RDTXZuT1NnUmlhdytNNVNMb2tIdmhQR2Viclg4TnlyeE1mMGdVY2QxaWE1Ym0zWUR1bFlCSkFhalh2M295OFdaY1Axc2szVzFJR0FwY3JvcWp1R3dCYVd5NFY2WjFFOWFFTDNlaWlUb1RNaDJjT0FXOHZsSFVaK2JrckJnU2hGS1FpNXI4Z1hOeVk1OUxJcVZWVE10dWszYXZEYm5BNS9nWThtUkw4TjhjRXNJODZtTGVFMUZvb3R0d0tSNEJrMTBjd1Q0a0ZZc1FmVE1aUW5vOU5LWmtQcUEzUllOZkt0OTBqT2NYYkd6NEkyTlBGYVdpb0cwL1gwT2xxbmR2NVhyeUtUb016S1A4MmZEN2NkTzFHYlcySFpvaUxRSUw2Zk9ELzVHN0poODdWcTF2NGcwMEJoL0FMbVBtY0R2ZnVqTnY0bVExdk55RXQyaUpKaDFWSWxhcFg0cWdHUGwxZllKT3ZsT2NraHlNZXNSTnVlS2kzTkRyMHFia0UzU0hKMndJRW9mZEJVRWRKOTNlVzVPenRZNXFmQ29aT09NdVRFWjlISFp3VFd0bzR4Ykt6Tjd3SndHSVRRNVJnRWZZcFc0d3pNNHQwMG5KanZ5SW5ieThTd0JPNTB1NjFWUjYzdEpaeGRDNTRDUC91bXpFVkxLb1d0SkJtRGgxanlVTmRkU2ZRS0lBNEVuaTcyUHlVcm1UcWRaYkxkdHlRSy9PRHJmK1JKS0VHZi9JbDNNU0xTM0loN0lpTlFLUkRPRmE3OXRIcGNGTFk0K3FJM3JTNGRmTEk1SVJZUGhMWi9jODUvUXVCdzBFSk1uRUh6b2FHYmhUOE5YZlNqWWYvV1BralY1VnJNd0grT1gyYmZFWHRHL3FrTkNtUlNOSFdId0lXdXE5Sk1PZTVpVk15MjhJUldDbG95bTN5L2xZQXg4Wkx4MjE2ZFFNYnZWSnJqQ0JUQzJsSlYrNkRuODcyWTJ2ZnNacWpnU0J0bkV3UGkxYTVxbG9ZZUg5b3c4SFhIb3ZsQndTdlN6SjFkdXJMYUV2cUVtMnJHOXFOVUhERlZyOWFBZGgxRFNVT0lBc0MxSXc4U1hHYXhBeXoya2ZRQmt6d2RERmU2N0cwTEZTMHVqNVdTSTNpOXFGell5Z3IwRjYrYUU3L1djSHVWWHh1eVNwbFVlejdOc1NKNmpLaXczbEdhekpVcjBpK3g5eFc2dEtTSlFmSjVzSnBjTnU4akNJZUtERGRhTjliMzBjNytWRDBqdHRkZm5lMTVuLzgyd3F1c0tVZlhJdmcxb09TT2FEU09CVVBtZFBCcTdCWmdQNUFnOWptU3FOTEI1R3YwNHdOL21zcW9LTWx6YTdSYzdHTWpaRFhlV3VvRHllNnBzZGczK2sxZ0ZxcENrZVI0cTAyMUhhM0JtY1h3ZXN6MmVocnNLVDJVU3hBekp5b29Lc0JEK2NycUVQd1phbnFNazd0TllzeDI3cjdtUlI5NFVNZWxpaEcwMWZ5aDZrOFpMeVlVbHJUd0N3NEYwTFQ5NGdpVkhocGVHZzBjRmRqdC9iNk4wbWZ3amxiU1FtbDlLWG5iM3Y1aVQ4amdJQ1krVDRkVDRvT3dWZ0tvZ2tIenZBbVgxamhRbWZVL2tFQ082Q08zd24xVHdSa2Y4OGFIYUhDWG1YeDNzdWV2am1kcEpibUtIT3BxdGEwekd0NXJuSE9uRHo5bXNTaGZHZDhJVjBtRkZXYThaYktnSXlkRjNQTGxOWGpiRzBwSStPbm90TGplc1hNNFVUdUFqTk4vWnlVUjdGSEdIZFMrWnUrdGszdFEwTGd3YWdUUFJpTjJkdkVMTnhoNmxMeWJKZ3ZibWhCMmUyZTZQTHlQTWZCaDVyMGV4eE5JbURjZ21zSWo4SHVrRmxRK2JBL1RQLy9lNWxNMzNWQkQ5MHpaek9GVFhGditRdzZ3elBncWl4WjJUUjRQQTRIQjVlUXZJOFFIMVNpeDVPMDczS1lsWDRFOHF2RkZxNmlrT2xDSVdZalhDVVFtU08yMnhNSVZlODNFUHgyeFVwUEJPenVNNDlmaHd0K3dlQ0RJZGRBZnFERDJLSE9yQmwxQ0dwU0N4L1ZoWlVtN1hJRXV2a1VwMXk5dmRmckROR0lESkNCRzBrdlVyK2lXekpDbit2bXhqTEdtWjVOMlV0bnN2dG1YK0hGMks4Ukx6M014RHJqU0lGc0VIUXcwYXFiaWtXRVg4bHZERnZrN25uUXFTV3NqVDRsUFZvQlJjVU9zaFRyU0J0Ylk2WWlKRFZuVTQ2bUNGMDRDWDZpcFkxOGRIeEFxemRzNkRlT1p0N2dOMUlmRHg0UWYybDhocWg2Q0wrNlByNS83cWxEVzhHUnVwbmlPT1RaVUdrQlQ3UDROM0R2S3NpZm1YMzJIU3ZhRVA0d25mVDZHN0F0cWxuaWdVamZ4UlBmcDJMQnBHc0FBajh3ODdDQ3BOK0N2MW5MZklOOHVTSWJiZFFBREtqamtYT2E2c1FmYmxCUkFBUk1LeHRJeS9POTVMWFowMEVVejJBbUN1anpyVkVFU3JRRUNVS2pXeGlVZXlpYUV0ZVQzUVlobE5QUVdSb1NvZTVlYjExem9nMVVuQ00rMUJvZkd4cTJMUlYxSTc3VEV2U3Z2RjMvL3BwZ0pPeGRGVkJiU0JUQmpKejVXTlBNVmpHWHpMQXAwbVZKS29vcXpGVjFnc1pxYitud3FlbkF0YWc5YzJXbW1aa0NhMmEyWnJLdjhMcUpRMUQ5NXl5YTkzZEVnQlptVG9kWWw5VzJzYzVUNnNZQ3plRFJYOUdxdGJsSEdXbUtlRXdBK0FVMGsrWitoNWQvd0J6RmhEQnlCckVaWEJFRnNVU0wxeWlCUmNtZDRZTWZXWFgzMVRFUE5CUmZrdmYzc29rbFc4VGJnWC9TS2FQRktmNDFCN2hleGZobzZDTnJhUWo2Y3VScm1JZ0huZHdldks1Z3RLS2x0NDlGTEFLa0pRSVNoVEM4amFRWlBwWGxiUmk2Y0JxSzBnRlBEZHNveU1FeWc4SFJYT2lrS0hwbWhCVFVUTG8xYURTcTJibVNUM1lad3dUMjN5T294dnl3anVrcHNZVkxPUWFsZDFOZkFBcDduNThsakRKajdCQ2xUK2pZTEpNZjE5eVhCUk1rVDBJZ3NWMTNzeUJFMzczS3RSTWhLOCtyRU9sL2xSekNwR3ZTMUFaLzZMU0JpUWtZRnMwQ3NCTUcyc1lJc29ibzNkVGxYU0YvV3MrZURFdGdZTFdmSTFwdXFoeTFNd1h5VWdlc1haT3dUc1lsL21HaUdVVTdzciswRXFtZDNSZVJ1SkM1d2lPK25aem9OTXJ5LzN6SmFGQXc4djR6SmpIV1BiQjZWOE5vTkpSRGJnR0c2bDN3a21WU1JqMVY5TUg4N0FPN0l1SzRmVUt4UXN3VFhkblZoNU9ubUpNNDdlM0xrSVJEQ2t0Ym5wMkNlZzVHZkh4WkpSeDRxekxMbFYzbkdSZXVzTEJJVitOQVpnOTJERGJvcWp6QVM5WXhOUzRBRXAwWG8wNnk2d3E2aGRsbVlHOGE4bm81S3duL1hyNUQ4Y0YvZlgyRGphUmkyVTY0ZEVLa0cvdVRDZVNDUUVuZFR1Zkl6NUQxTmpGVSs1ZUVVck5ac01yUVNqWjNWbzRkZExzOUk2Qk04OG9FYnlHcWlTSnJ3SHkrNTduRDFzTitGZ3BhT1ZjSCtpWTA4MG0yOWxudXhEdXpvN3RMd2ZQandVTXRJd2Mydm8yZTkrR3JTUldlaU1aR2d2cVVQQVIvT0NwWmF3REJ5TENGVWl3N25PSjNYZlZQVWZRYkx4dkNKckdzVjdmUDNNeFdpc2FzWkxoZHhXZnRBb0ZMMERGTUI4NHIzOG9rWjFZa2p2Qkw3TDExM3lPYVB0WEZWc083ZmY2VUZEcWwrSS9RaXhCY1o3TWZ3VEF4bk9La2ZBVWQ1M21ENi9Wc3FFYm9tRHpTZ2FuVEpKc3puc3VMNXBNYW5EQ0JZSmYxb3NvSWxTZ1VzQWd1djNhamN4REwyZk1uSWdsNGV2UDZaNyt6cnI0VGEydm1kUnVudUtySjkwQ1dER2hPUkx0Szg0ZE1FYXBDeVNNa3ZTa251QTU0TmxmRXU1cHJoanJvRDNXYWtvaGcrN3JTYW1iK3MwUmhRaktSd3dMakFJZjdqelk1bkpGQ2pPck8rNDFHWXR6SVpkNGp4ck9rbEtncC9xOU1JU3VFR2oxay8rN01reWtjc1BiR3R4UVNOT2dGbXFKbXY0ZU5JOWprRkVnK2g2aW90YkMrVWh0dTA4NXI0OG5FbzFNeTQvcGtOUGo0ZGNhdDRPK2UxcGJ6NDZpQkkyejJWbStEQTV2ejU4NjB3VjFsMWFvZURBV0tZOWtyaEQrRWZXbm91NC8rNkFLZ2ZKc2tHNFd2K3ZxNzZZK3VvbUxrdjZpREJFTXQ5aEt1YmpyNVd3ejZIUXhvcHRWWkdrUVJSeVJjdC8rMjh5Zm1rTkxuOW1paGptazdVWVgzQmd6bmpGVVpsRGlUNmszejZCc1RhOG5RMnpkMUo3Sy93TGNWcVNjYXpKQTNhY0JQSXNtUFB2OHZmS2R4RkZWS1lYeG9iUXhmMnhvcHlnVjlBS25YK1Y3aWpaR1gyVjZCUHh4eE1PVzJ1SThaUEU2VEpBeTF2MFp2MmV4KzFOR1FiNE9qQlk3U2FwTlRVL3VlTU1FWnFPeTR3QUNXU1VkNS93SE1QRFlVeFU0emFtYy9Ya0RXQ0s4cC9HYzNDS1k4SmlEREFwbFNPejJaN3c5U3d5b0tXaDFJR1h6d05BMjFiR2NUS09tSkhSYkJOS1o2SURRWXl3NUsxRU14THVEYmVNaW5MSkRWNHhHSklLanN3cHZvNUxGdVZiQkJnZFAvak82SkY1U0hSamU3eUFVTlY3clVTQ05PcmNUY2xuTlRNZHl2WDRmOWVFaVVvVlVqNlVBTGU0U0h3a1JIVVE4NTN6Z0E1Z1JRRThLU3hLbjBVRHRoVVpiKzNtS1N0WVpmWlBPVTdYWndYb2lodXFBMElKakEyYmFGbitIMkJVYmw0cGxDZzljMUIvNVk1NDZIQ1lEY3Z4a1BYcElGbEh1WWVNVFp6TzVhUFJjaThSTWVXbEZBOExhUjdkZUU1YkRxdjFIeG1SVFNtNGVNVUx5SU1kN2psb0NlYnNJMktpSEhzY1RUL2VDRTNHWTZXL3IwMkVSYkFKWDdXWU5lRWJrRDZadktkMmFaQmxxbDVLY0dpekplTzBGYlhvN0taSFB4OHlITTV4WHJNTUpXQVc2b0J6SlJXcmpUcU5JZjk3N0ZPSWNVS3FVN3lJQnNkQWY3YTQrT25qeWttL3dkbnU0M3pZeFk2R2RzWUJmdGZ4cEN0cEMzRFpXRDFwTHhDb05MMjRlV3Z3c1crcjUvSFVCcGNJMDA4L0NtYzRMMjNUT1p5S2J5cXVJMHZYMFVyN05YZGtMRFgxSXovT1g4eGZpdkZkZGpzdkRkdFZqZzRDcjRITXlwTnBxaFpneVY3QWNENHd4WDQ4M2tmMldPbE54TDF2Zk5WMGJMTHRvT2RiSkZzZ0tCamdubU5nclBoNlRtWnI1V2NiUVBiYStNcU40Wk1aeGFLblMrUktyK1hjR05MTjZmdU9ZYXBiaFJMK3pEaFA0TVRPYnQyRGxMaG40MGVsaXZuaFFyWWwvL2JiNWNHU1NoRVRvbmVXczlObmVadUdCc0JOQStpdGxHVlNDMk9keExlMmo4bkV0ZEZENks3V0Q4ZFppcmlzeTdmOHJXam15TUJsUithSFRmMWZadEZjL2VzaGUvOGs1SWpOd0FNSVdsQWZlejhOR0JFS0pxWmhUTU5yUk5PZmd3eklNRGpHRjc4RXE1aHpXa2grWGNUM3NHZnJNZFk4dXVyNW4xRTBiTzNjZjVhRThjUW02MFNKMUMwcGtEVWc4NEw4NE9IUmFGbzQrUkp4UU90QXNjRUM2QlNqcCsxelh3eGc0Qks1b21jdGREUHlrVGRSdE5NRTB0cFg1czZvZWg4ZDBGMUpBMnVMNWI2VFNNSkhsaUY2SkxJdGMySFcwVnhBUGdrWEZQRUIxZlR6ckJTS0oyaFArZG80UUtqSllrZVN6ZnlrYkwybUdSb3JxRG1qcldHSzJKSDQ5Y2cyeWttRkVvd0c2Uzg2WDVndWRPYkY0NnpQQzBRYk5iK0VKanRTeG9sTkR1a0Y2aDFnV3M1UzFTYnZ6cHlUdFBqUkY1TS9UUnJwT2ljSUpPOWhhWDI4M3pBUWNUb2JPQ3BwMDZTaWd6MDZnL0k5L2JqYjJFVjBpMlk4d2NoL2xPWGVnK1pac3ZvNittMlBSTk1NQzVGYkl5eDZISDBXWXNNeEhiUHlVZUJWN2lrK1ZNNWova1JHM3IrQWcvRkx4SEJiNWwxb1JxUExHaG9qNmhqQ1E2Vy9zODkvc1VVWGZBN1lTR3d3bnZDRHpvUjU4bzJzSmZJZ0ZzV3hjWHJSalMvcHhheExYMzY2L25GYlBZZnp2TUt2ZTRmb3lNOXQyR1JDbVpTdGFTUkZsTTFONS9aRmFmVVdzanB5V3lPS0ZuaWJlVE15YTVEUDlNdDBnNVUxNzNGS3hRZ1B5dW90MFlKeDNGeGVpVFhZUVE2WFZHNWFOZWdrNWUwalV1TnlBT2tYNzA4bmJUdEIrOE1XL21NMGFuaGc1bmZidEh5dlBFWHdpT2Yyb3hsMW5DcXZTbjhsV3JIQkFSb21VZlZ4VUdIazNVWVBtWk82UEZnUmVJZ2xRT2cwQ0VDdms5N1lvcEhXc3U0ZW5LanhXTXp3WldwcjBPZEJjUHN0NlRCbUtzb1FsL0RrRXJhd3ErbXJVWHNuRVk2NGQrQ1Y0TVltZkwzNTNvMFVBdDltYjRMYkN3cWxxNGFCTTU1Q0x3d0l4NGo3REVMcGRTTXhtM05pTDhZanIvTGliR3RoanhUcDVKQTJHemI0dk1WR0dxWUlxbUtHRTkrUERCYmZPYVN0ejR4R01PNlgvYTZYR284N3VPVkJLcTFOeVh5bkFLZGxla1pJVmJYNEluT2hmMVNLb0JHZzE1czBpbFRTakY0Y05keDcwa0VtWlFtNGQzZHR0MXFsNWM4NWk1b1owR0ZUdmZpL0Z6OElDdzF4NjlpUjA2cy9jRDRTd1NmUUswdHNUV1V4SmEyb0hlTVdBcEZOMWo3dlhnMWpSdWZWYnM0TXl4REpJZ3MzRlRaYUVYMmdmOW9mbnVZR2FGWkF4UXJPMjR3alNzZ1Q0VjEwTzliaHZORmxmWGFXd3dxZm1UcEc4UHdCT0NLNi9ueXkrZXM1ejdFR1dOQWhmbTgyLzl0UVJadzJ2bUdsZSt6a1laSHJZeUUybEVveFFlMGJHZDhCMjZtZkYrOGZPNUttT0p3WGRLTmszTXcvU1U5MG00WCs1eXdiYWgwNWM4UFluYmNhVXdaMVJhM0xEaVkxQ3JpZmFydnRMWHE2bGFLVnRET1ErSjVLSDZXeW51WWhTZXRmbnliTi9RcDZ0eWEvczA2VEVaZklucWs5Tm84RnJhY0hsNGd0ZGF3alYrYVBiSVNzSmZRL1JqSFAvd1ZURENpSk40MEFGNG8vR2xvTHJYSWJ3bUNjT1VWcmRZczFTUktlalJxM0pmZkVQekdMNDAzU0JhNWpLS2h6WWVxMEkzbjRvNWJEaDFMTmkvY3lHbWltNnFGdFZJaHJvVkFnNEp4MlphVzAvN1crRWRjMUdObXNuRGUxdklDVWF5UE1xOVd4TjJYYTlFUHoraWVBeVpZTW1Fenc1eTNUWUZWanZkYnVZR1owYkNGVXE1dGZvTFNtZFJNNjM2MDNWcEtaRklob3JIRlpOOC9Tcy8xQ0JGR0pQQ01OWnQvS016MXBCZnRDNmZXSTdobkpZdTNaQnJFV01qTFczc1BoQWhrRURaREk1ck9VNHA4RUNUR2plWUVhRGt0czVLT2ZVeDYxOTNnTTBpdysvUk9RQ2RJcG9NRlJKOXY3QlQ0MjJTWTUxSlhYaG9sQWZJL2RhdHBvSE1lZVNKcDVNenRkOFVjVENPQzB5TWsvL3ZRdGkxdVJUeFc0SkMwQmZuYUlXdTRhR2l0WmNrWGtFcXFZZ01WeHhYWDVmbXREdDVwZHBCV1ZCbitaYUdSRG1PVjI0Qk5GcTlHc1ZEbitoaDdsOHZuTStPOUkxcWRVSXM4L1ZidlltRnlLQjNsTUl6Vk1MdUJCMnJkY0FIMWRYaGdnVHFWblJ6WE5LM2ZnTFVLb0RZOEEzQVNkY28vTzF4cmcxRzUYAQ==", + "server_vault_b64": "CAES+JoHVmFHaDlMREZvaHZpajFXdzd3OE0vY0cxZzIxcXQyZU8xWm5mZHA4RUhZcFdJeVl0ZGt0RWxRTS9IZlBsZzI3bXYxMk92a25SalZIZFhUZjRKUkZaK3ZhZkMwb0F4ZGJEY3FKa01PNGtaeEVLVjJYT3B4LzhUeklMMi8rS1JsWW1pVUN2T2Y4SGE2aTBqVW42cUhLTHEyVk51cnNHT1ZiVWRGZ0NOelRUUXZKUkxLNS9LcnV1eDF4NklJWU9ZOVVJaDVTRFk0UG9ZV293QkMyWlcwTFJYMDhlWWNENUpSRkQzYmVZdGhObUhKR3FMaGR2cUkrZmFTVGp6UVpkWEtvZWo4UHV5YUxjdnpXeFVFSWtqOFVNZjNDWnluQmQ0enFTMGlVKytPc1pkakZSUE9memZZbE1QZnl4a2xNSEhZU0NsbDdCM1VyOWFPK0I5RHNmZS8yTlFDRVpmMFU2NG9Qc2RaVzZlMzdlZzRBbFNxRWZhQVdkemNPdXZYQk9Bc1FzM2lQZEp2RDhlN281UERNeXN0a0llOFE0dndsTS9iTDgxanRMaCtzRmhmK1ZqZ0ozVys5QkQ0QWR2Kzh4dEo2WDRxSXMyMGtiZFQ2bUhsdm50aTR3N1MySlFZblRieTJ4RHZIQXB4SHNIcmNuT3IxeFllbVI1UWpDWUs3VFhiSjhxWm02NUxtUUt0Qmt6QlgzdHozL3ZrYiszSUhCZ3lzdGo3dHFKMjk1TnpzTngxZ2Q5bjA1Z0lIVFd3MURYTHdwN3RGaG1jTGwwdzVhNlVnUENzcnA0Q2x2N1hJZ1BoeEU3OVVRNmFBSlcyRkpQc1huNVQ0NEcremhRbDhiOWNBWk8rTGg5THBqOHlwU2ZkRi9ZNWRLTzJzSVlSVXZoREp5QmREQW1kTHdZcGVuNjBzbnpoRkswMXhYUzFKZUY1eGs5VW1WTlNpaVRINnNNRFEzcFJhK0Zwb0NjZnlnRHduSENtK2pqbjR0Mnc0TXZxbFNFTWR6aUVtUEJuWWxNRno3MEluQnJIWTRMNjVsY2lVVDk0V0ZhZzA4dWdSOWIxeWlhMllVeTFpRjNZZmU3dlJkZjBnOFVlVE5udlQ3OUh1YmUwemdLTUVoZzJJM24zZkRqS1R3Z203c2prSlo1V3FRWlhxZXlmcGsyU3d3dWM2elhqQzhXOGlOSzI4bDI1V3ViUWhsTGQ3bUsvY2RweDNQMU9Tb1ExWjZ0OEU4RFZ6dFRGcjI2YS9yRUEzZzZQaWJ4d1FkbGZWTVgwWFdJcjJZc3FaVTVJd2pQcENmNUJUMnVzWW1jUnRFN3lNVmRuUnUxejNOalVVaWRlWUp1ME9TRmFwYnk1NDZib2RNd0NqL1pLUnRqNTA0RVE3WkxKSmlJS3hZWnd2ck4wZHU2eW5qeGVTRVFPRktHa1czWkExMnZuWUhlLy9LOE9sMEdLVWlsR3kwR0pBNnNoMjRlSzVXZlpUZFNBVko1S2wxQkRvL2Rkc1ZrZmh5dmN1K015aml6MUxNM0cwdEgvblQ1QWc1ZUE5TnFONXpSRVN1aG9FcGVrMWUwN3p4anVCTzM1N2NUdjgwNWJ3aFFvYlNrT3cvdUFVbFhKRFlULzVjSUZjY0xvZjR0MTNzMVZFK1VFc1FWQ25mK0JQWFNkbmFwaGk2VXkrbzZnV0Y4YzhJazdKVDZRN0tBbHRvSUdQZG5Qd2tGUERuOGNlRzJyTzNmOVE2emhlcTJVbWVPbFh6cXNSNkdVR2hQalFrMjhraTZ6K2hoOW55Q1IyYzZ5SWduRE5nRW9GUVhPSXpaNXZHamhXaXFFZTMvVjBmd2hHWkJRUUkvanAyZkRwbktnVXZuZGxnOWVvWGlTMi9jTlM2TzV3MG8xZ3FTRUlSdGMwOTB0cmhJNGlnM25FaHlWMEljRmZacVJzekZ1elVvdmJQTk1uMDVYdTR5NWFEVTNIK1lWTWV2cWFISzA0K2ZlMGtUOTdIY09ha01LeXVLbVozc3R5S3FsalBkc01WNWQyb3pWdFdrSWIrcFdMYWVPUThQbmp6b2Q1SkhtRTNaVmozTHgxcWpwbEhCSndaWlAwVWs1SithdWUvc2FyQmlhbmR2WWs1blhlMkhiWTZlUFR3ZlpWakhXTENMWEtzbkZkWG5acGFpa25Lc0lIYUJhMVFlNFdzVVBzQXlBOStMS3JhVTlLLzkxUUR1cnNtSU8vanRWTmcweE9ldlFUZEh6Z0Exa2JBRlh6NUdod1VIQlJDVzNtaXhqVVVqRDZoeTBGazhQb3BrQzZtQnpqOHNjY0pVb1dmNkJQWkxqNGFEbU13K1l2VUpLenlhK1lHWDYrazdMbjcwVDhLTm9mdEl3bnY4R0NvWmRIaExzOHI3cE5jQnVTQjRiNnNmUHl5MS9RejFTQkx3V09rYklqcGFycVBRdk5jbWtXbW1CKzBmZCtwOTh5YWdDK2FKT1kvVGQ2T0ZjRHhOdWcrVXFRQ0hBYlpvOWRRMmpyakwzSUZUQnE5SlFDamVFVzFWemQ3U0wyajB1V1ZORllrWWJ1N3RLbjVDL2ZIUlVEM0ZaUVE5NDdvYXFsM2cyUUFhSTA2Z3Z5Z24rVE8vNEFUaXFWVEswbGRwcCtpMFg4SHhYRHhzdDBBM0l5NENyNFE1WjF3OGRvYjY2bHFWSU1ySmg3QjBWQkkxTHVpUmpieFBCT0p0Vm5oSUdkaTdwbTVZU2QyclFGT2l0YzEyYWQyNlo3eVE2Q3c5UHQ1ek5naklDRTdXbkIzVVFSR2lSa1VpdVJhY255d24wK1VFYUhNUXEzZ2k4NHJQOFNpbGF3dWZESjF4dEV3ZEdtVHM1dG9YVkkrVHFUSVpKL1JkYXlyUHRKclljTGZ3UVZzNzkxaXdtcWc1RWxTNk9ZOFMxMkFHcnc5Wm1CcTBBaS8xTWZaazJ5UGpwS21RWDQ3S3hIcmNqMHlVekE5aEtoSTRNZGNaamFERDY5N1BQM1JuWmRxRlYremNNeDBmOENZQW5mbWNnbGRWZDg1WDlBN1I1UGxJWWpVeFpvT2VGUXlieU1VYm9BWHF5eEw3OTQ4VTFMQ2tOL3FBZzBKWW5JMDNMWERoZlpQU1FtOGMzMFpkdnlWd2p1aW9Ya3g5eFllR2N0bUVBa1hKL1Q3NXJOUzRIZ1BWUzc1a2t4b2o5Tktlc3JudmF4bFpZNVlTakd2RDMwNG02SDBXbG1FaVFUeGRZNUp4VjZDWVQ3RUwva0hZTS9Ob1V6ZTlZREp1TG9DdEREOTIvU0l2a1U2NHVtaW40NjdNMkE5SCszSGJaOG9LRFFWS0dQVkJma3lwaTFvKzN3WTVXQk9IZjIvVm9vV0s4VlczWHEzN1FIUjlwSFkwVlZGRVBheEVDcnU4T3pYWDUvdW9tM2NKOHdWOEowQy9MQWZIQ1c5Q2F1WG9TT2Q0Nm4rcTRrdTJNVVBLS3Y0cDlsck9LQ2pwczkrb0hMVnppRnBmanllTDlDUXFGMThWNzZaek55Uy9iY2g2MzVjQVZtcmJ1bVRpVWV1eEZrK2JtdGNCU3JQalM1dzIxeXBFd1lhL0dJSkNvcnBaOE1RdS95SnZwQ20xMGRDRGpHeDM3T1AwRjRhNWU3Y0c0U2I2U3RUbGJpb1NYRDQwNmdNcThVOWI0MWNVYU41c2pYd1haRXk0M1FSV0tEOG8xeUtEc1pORHRhTWdKUVpBWUdUSmxpY04wY05jY3Fmc2RqWlhJWWZuakpacXNRZnhWWXpZbUI1ZzlDVnBHdkYxOWNnWkM3Nlc0bndOL2FwLzVXd1V4Q092RUlCOGhYRkJGTU9qTytiNG1BNENwRDVxTVJkV2tDMUQ0YXVwQ3N4cldpU0ZiUW5laGd2dkFqOC9iZXZMYXkzVllZQ2dabHVTOFh0Rktibk14clVqR2Y2KzhaNTBxQXV0VHI3QVM2aW4wdmgrbzFwRzRqTitPbERVT3M3K05BdTBHSWk1MXlQTFBMTkQzSVRLa1gxVkJINXRhMHl5MnRZZTYzQWtWWnhUN1RLSFNaTmhhSnROWks1Z3VCYkFEWE5vY21QaFEycGVNNElxYlhpSENpaGttU3ZlMVU1OGVISnpNOU1yOVBXbjI2RE9ZWWhTY09RS0Z0RDVNcWV4OHR4ZGNIQUliTHZHYU02S2JSM0VYcGkvU1pVWmdtVUwxYXhJWGw0TTd5QnhWNGpOLzFTaEFGSFRaU1dHbUI1ZDFHdW1sTFNPZmRkbW5hVnJHVzRoWGpITVh6WUJicnUyWDZ2ZWNVcXcxUFJlN0NTS2I5TnB2QU1VYVZxZjVtOGl6RFg1S1gwMWFHKy9palZKN1k0dHQrOEpGaktVUXo1S2FYcjI5RUhkREZZOElKMTN0NFh5blRDNjc5M0tNbzlVa3ZFbjFGc3VvZWYzTGFWY1MybDRRTGFqQldxdVcxNERVNjRSQ3d2M2V6TUVCcW0xV1Btc0p0bjJJL3FpdVR3dEY1anN2M1dJQ25XUXB2OWJFUnJZaGZYNGNGWWNuVmx2a2VXYjlSMllQQmpDSTJpdENzWHhJM2o1QUNTWWFSS0dtUEp0ZG5ybWVDVCtzOG5TQmtNdHIyR0Z2S1ZkSUljUE1YOGQ0N1JxYkRSeGt4c3hjc2VwZ3BYK1RiTTZXeE1NOFl1Y3BRNEhoTEl0WUJ3aU1FNHpiNXdHcGVDYjJRekw4a1lXemh6dkl1QUlrRGp3ZWpOSFljZGVwNFY4dHdCdHQwT3RsRXJhdUpqSjlOVlk3a1ozYjlWRjF4Mm03OTFjcE9seHFyV0tLSUNZdkFkMFRWSWYzZ3lmR1FMQ05zUkVidTBISDJYSS9PMkV5MHp1YVZzK0hrV3RmUjdUbWtLYzE1SmVLUzBCZm9rMWwwRWhwaVJObXJuaFZ5RWxkTDhsUTY4aHBiMnNhWlVnMGp3ek1Lc2l1TkEzQVEweitLQTZneEh4dk5hY2xQaUxlSVBJbDU4cVFMajRKVXk5eDhFOHJvN3hZVFhxY29Hb2thQnJoTURmajE0SFFwQWhuVy9QdWxBUjAxZUVMMkFtdHY3L1E0L2dnU3FrK0JJWXJpcGtvbWZlU0hLaVlFNkJzZHFCZUZGN2lpZ1F2NzJwdFlKNkh0NXNHSHRUNng4VlBKaW5DbFVSRHVSU0pRdkZJaE9iNkdIN0xWNjhlcGlRdG1xajlBTjU2enJWbHpvcVRqeGJRcGV4bGx1VUFOcWFEYlBRTWsvMEdoSnBlemwxYzV4R1J3c2FoNFFaem1URlRnSTE1NWJ4K2N5S1V1VWFXY1JhOWlIRnZydE1JVW1MdHpSYUNpTm5YRTNFZXUydDgyNkVUNmNjOUdhL0xyWThKeHh1Q2M1YW9jSjNKVVFrR244dW5KSXhYbFpxbHgvUXNrY0o4WTVKSURsL0ovWEpFWEptZ0ZRdy9Sc2xrNVh1eEQ0M2R2MEN5Z0pyaW1LK3U1YzNqeEdFR1owWUVZd0Nib1hyT2RLakxXWXl5YzJFVEhTbmQzNGpYM3lGblNNczE2dlhzeEhyMUhZbkJlUFFUNVhUM2d2a3RodnZOUXBLZ3lKUnozN0RjS0RFSkpjamhkaEYyK2VFcEUyVWpEWUZKSUhQOGRXZ1dleDZUU2xPOS96dzlNVENUdzQ1RUtrNGZPVWFQcW5tQjJIWmt4WVVKNjFzd2k5cU1YOTNhYXBkOGhuM2hzL2xjWXRGb2F1czhUNkJBNVZtbzZCeXlpQTFJVWNSUUVDM2ZFQVIwWHR2dk9qMXFCSm56L2kzR1VuVERUQkZOVDB5Nk1kWldUT3N5c0cwV1lkVllGN0tzTVAvQWlBTUJqYjFOcnlsMEJoZ1FTcEdSbStXSnplLzk2Q3pvWHBRZEVIOHJ4Wjg0aytaL0NVRTlVMExHRW1FRTlJcEZ4TDlKd01RSnA2YzhyRWxnWGo1dUdRZVd1UExDaEdnVkp2VU1kUmxjNzhoYi9jdGZnTEtTWnNRWUgvQ2ZjS0IvUkx6anlZUisxdnRBVm5MZGZwcjVGK2NDZ2pLYThOaXc5Q2tNZmF5d01PTDBkWEtaeFRjTngzQ3VnKzU5Y2FzelhFSmtIajUwWWVTZUJyYUpKOE9Ca0lGU1ZXQ3RQdVhEUXR5c3ZZbHkrLzJzNkQ1NUxFekUvaktHMkJlMFBYc2FQbEhseVVQZHlYRDE2UmNiMEFoN2RUS3lSQklnUnU0TFFSaG9ibmY4TU1SSWJVVm9YYm5ST1VqT0dBTEZac1BRS0V2LzNnK2YrKzIyYzFaKzR0OTFKL3NsdENlL2dnK1VHRUM1SHd3d21sbUJnR0xoRWpCVTFKWk5ObmxBclpvRkc1QThZL0VwNElDMTQxQVdDVm1NaWt2QkNpbVZINmlUbzFzNjVqakpTcVM2OHpyZXEweWNYR1BWaGRzejhsSVB5MnA2RVFaQkcveklWODVzSE5oYmJNNCtRSHFtM1ZZNnl3WERBMzUzbnFpaFIzcytWRjByMEowUUIxcWsyMy8zWEs3aFZlVmpZTzY5Zm4vMlN2VlVkRVJFZ1FWNmtrTVFxeFVSeVpKM3VJUjAyRDM4V2lxam56VzU0cHQvY25oWHhkUzEvQUNISURiTDRpcE51Yzd0M1RhZ1BoYngrZmcwTmVjazMyY3RSaWRDTlVLaE1yZ0NGK0RnSHB3a0RnQkE0eStPUWR5TGV5QzdHdUJ4dGJ3NC9UQzN0bW9QT3h6cElKcE9sWExxMHlqZ0o5L2lmc3Jsc0tkeHRBTFpUZHJBdjU1TGJtM1M0UThZSTd3OS9NTitMOU5BdDJ0Qy9obUVxVGUxZ200S2JrUFFBKy9YbHBvWUV3OEExbHFqQmhadlRnMHNEVldvTTIrS1R2VGE0aGlocWh0RVUrbDE5OHNRVE5rVW0weGNHVkZ3NENvSmxCbTRLY0pQbGFIQXJqdndWUDR4U1BCTnV1SGsrL0FVZHZPZmhEcXQvN0kzazY4QmRDa1FVbHhrdFV0R05yWTVDZG1aemtlRVlkZTgrd2pjbS83RnpTYzFLTUoyR2VJcDdIbFZmOXU1dG9JZER5ZGQ5bEczaVQxeW95b1lSK2NOQUQwQTFwa0dBaXUyTHMvOE92T2VabXFFQzFXUkxGNTEvcEQ1NFptMllyWnJmZmtyUzZWMzZSa0MzZjdiMk1rVW9GQlB2KzZQS0xDTnVDL1ZFOWxZTlVGUHppRnc1UDc3RHU0K0RPU20ydnRQUkorRUZVUmVXU2lLQk9vRmFPNjQ1ZGF0cUdHZnNDUktRMlIzeVJGWGREUHhlcysxRDdmeUZWYmY4M3dGdmNHNklHL2RhNGNkYTZJSEo5Kzh1cjZHbHJZSTBYQzc0S0JaOUFwWG5IZVpmVjZFa3ZMWTZMcDlVZ1hCYXA3K0VWQllxUFBtN3VzMTNabXJSVzZYREx3Yk9UenF1L00xbm8rOEU5T3pMUVdyR3Q5ZmxWb0R5cTBva0dIMVFFb1VWSDRwK3VhUE04eW1EdHF1NXZmQWU3dWpkdzBSZUdiRVdpbXdIdEFhWUYrV2ZXNTZwcTFOZG1BaXh5SzkzVG1sQkpYb28reWtzS0hieDBPclJRTTB5dE9QUFFFV3FoNnhxbCs3ekVNYTgvWXJ3OGlPRmFhMkl0UFdaNUNBR1JDVW45c1ZQdTB2L2ZVK0taTmdvS0NvOTQzdG45b01QSE9XRkl5b1JQRnhyN2VleHphMEhCNlh0ckZzV2xUWG1KNmJTU0xFMDQzZnpwQ1FGOGJDT0lCaVhhRWowL0dlNk9WT2J2dmN5bURrNUlNaDdvaDExYVlSVWlwaFRIamwvY0ZRTGhIdnJGRFIwQkZscWdlS2l3ejRISDNKallwanhpci9uYnpDcDhsd0x3YUtJcjJFTUdCRWdzcDZ0N2FFMGRuNytCOEhFSFVxUTI3UlREbGdFY3hsaWZ3T0Y3WFZBMHRzZnlzRzJiS2VQWEhqK0JSOGQ4SjYyQWZaUE11TFVnTlBMS1k0UDhldkhJQjRrUHh6dWFVc1Y1cERDdDZoajExa25tczhtMU1VRVJLenE0RW5TKzd1R1ArQjBBZEpQd1MvM2R6VW9lejh6UW9iT1VEUjVJcnhCdUpzcnVnejFjY3RQK3dFZ0NRVktDZjBjcDdid01jOVNDN0U4RTFRUUpxWUFVMjh4bGNvWXJHQW54ZGRlWEpKbDdJWW1oN2lxOGVzVlRnT24zUTdnWTF6c2dRWjdlRHNYbkRhMXAvdmhCZE5xVDAvckdVMm9tbzBPUW95YlJOdTlXRW9rRTg2SVZUM1FCdFlhY0Y5Sm5CVGNtdTB6Z2hlOEo4SnpYLy9yeWdFOE4yVm0zcFVLeDQ2ZFVSTkRwaDlVTXZhMDh2VEdHczRLNC94ZHV6ZE1hSVQrN0xuamN2cGJkYWNyVTNTbmJrdnhwa1R1UGZqczVnZGdKTHhYMFV1ZXJUT1pHMVkrY0NwR1Flb2l0bnhkUm9aZzFCTlc2cFp4eVU5QzF4V0dERTlSZDZkUlRub1J1NlB1Q3NiNW53UnI1VzJZaXNUQlFoaC9CRlc5TC9hWUtUVVJlQndrL0VTUzNXWUE5OFkvUENLRUhwWTd5MjZ4M3FFT1Q4SVFnZmdCK1dINmdBbG5mbXE3eUExYmRQOWEvVFh4TUxuY0Y2TDEwbytmOVJiRE01bmFIMHNMVzBmMXNLOGJ6NENSNWQycnlvcmh1RHpjeTlPYnBsRmt0S2pWejAzaTV0Wmx6aGpDbFgwOTQzOWNpbGc0eUF3OFFFS0JjZkdkaFZGVlZDN0tJYVVmU1AxbEdKVVh2amRLZGVNa29BMWREZjduNldwaUFIaDZnM1N4UCtxUHJMclZrRmFhL0VCcXl4amdoNnpGYlJGM1l2ZWdIMUZxZVlDVzY4bUJTQ3lYRTl0Y1haVFRFcWFSd2dZMm1JdU0rRngvVGFBRWZiRUErN29zVmpTcGxVa0RINnBtZDRtZWtrUWtHc2JqMVpDQ2o4Mkp0cUVFYUZJYXVrM3NXZjdTczhWZjBTdjN6MGp2SENrM05GS0QweTZmc0Iwdm5zTXRmZHltdlpraitlR3dPT3BqVldHRk10NitZbW9IN21adVVPUldXd0F1Zmo4OUQvaVcwOS9nN2pYVWo0dXRxaXViQnlkYnQ1blFtZmxqQkNsMVdzK3RDWnNmV3ZhdXpJVW5PbGcraGtkT2pZOUhLZzZ4SGxXVlFFSkNKekhSNnUwWm10emVzazhraEVCZXo3dkNSdjN0NDMydCtwWDBaMDZGVC9IOThmZVhkREY3QmVRS1RGQUYySlgyL1JjREsyKzQyR0NCNGc3aEI2SXk1alFHZXFNbCsydWdINUtDOFhjcUdpZU9FMmg3UFFvN3NpTFBETkZDMEpYSEw5b2tSWG1vK1MxM0x4bWpCbFdNR3NZcW1yYThOTVFkS0RXVkxqS2puYWFwWHM5Q0RWbWRRMkxMQ0NUUjFnejJpTzZXQUd6QkJ1VnlOalpNZWV4aHZXeXZneCtaUTg3Mis0djdKTHF2MWF3blFZenIzSDl3eGJTeDcvRnJvVU5nTmw3Y1REa3M3b3NxdnZrWHE4WVNuM0JmOXI2Yzd3T1p6Mi91SzFVMXFoOEpQNFpqSjR0U1NqUkUxaWZuNzFxblFWNWtiS2kvMTZJMjcyd0RTbVpEQ1NHd1V3SWRxOGdsYWxvTHpHbTg5c0lYeG52ZEFmMzRQMkcwT3dseW5JWHFYTHQzcEd1bDlVMlViNTdNQTFTTURHUlBWWTQ2QUlRK2NMTGRvZ3ZXTlFMVUNhOUI5eS9Ja3FxRWphbndEZ2FrdEM2Mzd5MlpUaDN2MWxHdk5YY1FJdVJBY2VMa0Zpd1pQZkRNN2FVTERCc1lRakNlRk52aklpSzZMR0hCTVdBajZkRDF0aFJTMWp2cUJaaWpDYkRwZTJMa0FuZTRncmxwWVMzK1d0czA2TEFBUWNZSUVFaWJ1VFRTVUQwMFF0U2Fpa29FVjNrdkcyM0pJOXlEQXEyUEpoSU9DcjhwZ28vb002QnF1RUx3bWs1elY2NE9vSEM2NjExYUpmN0tmd2dzOGNxemlyTSt1WXlnTjJqMGFWNVZkVWlLOVFXZU1GNlY0cDVCL1NJNjlVYTEzTlpwdXNqelY4Y08rSGVWRzUvK3R2UytmQ2s1SWhYeHFKSVlud01xdXgzOERLbHJ0U29FRFRRTWFpdEU5RG5ZWmZXenovYTU5Ly9sSloxTG41K3VFU0FGWXc3anBBREVscEVpcnBIYUdBRmV5a0NLbFUzYnVGYXNaRittd0V6SEJpd0NKejZDaUhhblFWWUpPWUZkNDgwR2tFTmc1bnpUOHFQVXlrZDlMZnlFVVBPbVhTbnJ2Z1B5b2JCbm8wVmtPOFNudXlGbEpHSjRqVkVGMTlqQ05VUlZ4b0tTSU44S21LOEtvNkE4RGs1STJYeXpuVkN5RGZ3bDk4ZFFkVktiT2UvWExQQXc3M3dGZlBUSjRaT0s0V3JGQWdKditRQ05wZWpGL0JaSzM3aksrU3I5ZXRsTHpYaWdrMG5qSnZPWCtINUd6U05xR3BvY0ZsNFFON20xMWVxWGhXaytwTEkrd0lCc2RWcE9xb2s0N0UxV01WZkdKdlZvQWNKcWdXTFNPaGsydkpUWmI1ZTM1MjFaaXorVHhKR25ScHJwNzZmSmNMK3ZiRHdOdmhFKy9yYllPWi96bHQrQkJKZ2NTejNWK2xsb0paYmRGY05WL1d6NWdtY3k5emNuaURzR2lmN1BhNG9YaktUbE8vSjFxcjFHeUNIUVpUak1qS2Q4K2t5ZXNKcm1jK1ppOTl6aDJMWnZTUWp2MSt0bEc2TVFHVEpFT1FsU2F2RllkN1RXRkNSRUVJU1VPSkcyeC9QZ3RPQ3lpcWhnUENadzF1ZGpPT1BxSVNIOWxVSHYvY3NQSGMyODBtOExtc1hoSmtXMUtIUWx3RExLMjNHQk00QWZMMS9oM09OUVdjSEUycW1lUFRCaXpkMjNiZzBUTDBMTUoyYlhsdlV1NGtaeEh6ZHlrMmYxaStmRXZqbmxKZUF5UndZdmorMituMGVJRUV3azRmK3FMcTJaNkhpOXozQWdqRkkwL1FQQVkvaGtFTk43bDc0NjQ4ZVFaQzFGTEM1dEY4MG4rL3N6dUFDRmhmeUZ2Y2VGVytpOUpvZ3BoMkF2Nld4eGZ0bGIwT3g5NW9IRFJmYVZCOFFjV3k3MjdQSWtBa2pwRmdPT0dqVDFiWXVmRkw3RmRxbGVWaUJTWmNKVTRPdkR1a21Rc0RPMlVpT1kvc013UFJkaG9vM2NFTjdtRXVVMFY0V0xOc1JkaGtWdnhzQUR1a3M1R0ZUU0paYzVWSzNpZndtb1VFMWFpMmYyQXRUZHdiYjB0NXFGN2ZIYUxzNytESlE0WDlmS0ZtSEpCNUg1TS9oNVlHb0pGcWg5YmVuQy9qb3IxbWt3cFV4TjN4RURTdGlYS0wwVkY2dlJjSjhTN1hLcVNiT1lBeVNRdjZsNUhwVFJPdjJmblVSZWxmckYzK0VHTnc5ckU2SlhkYk43SFFWSHdoOE1BM0FzRWhFWlRQVjQwUkZrUm9OdCtRemtUZjVVb1cyZ2MyeDIvYnZDRWhjc1NMZUdCdEFjbnpzbEx2MUZkSlJ0T1VMSjgrVFZ1YW1JUWF5cTVURGtCQ1dYRFpEdGxoK3lSYW1NSlJoak1aZENRNGVaMmlhNWJxWHFUSW9lK0k3YnpuSzM2U3JMTWlJQmpyQXQzeHRYMGVwaGFXQ1ZlcURvUERPWUVDRlpMRDAyWGNSRE9xZ2JyaWdSN3FsRXZSa3FqYUJoUjF0MnZETzcvQnRMTHlCN1JuRDBuSk5JTmdsZ2U2SmFCQUZoVmRGUzFWbzBXRVp2WHB6YlkwMDJsVndSRGFPa2tCamVydVVRL0dZZEhCV0RGV21MLzFGMUdZUGs5V2llNHZoL24wMFFOQnhoYVQvU2VxeUNnMTVya1JRSWE4WHRmb3loektXZ0R5a1JHZWN4cWhNWC9oSU5jd1AyY1B5OU5EWXBzbWwzSnQwemtjMG5aR3lPd0czVmEwZi9LVHNaaWE5dXhnZkdBclAyRkgrMVhwaDA4YTVsdi9FMWR6R2JQRWtHRHhLTzIxMUtmdDBqczNGeEdUYzVRWEU0MC9GZVJLaEVvYXM3QUdmSGdaUzFveFB3dXM4dG54Y0ZaejBLOUhLR0UzOTRFTjVJTGtRT2RwRFFzcWx6cVhYeEZoVEZzMDhJYy8wUXZKbjFGK1Nidmc5U054bGgxMkU3ZldJelJGQlh3WnJEVHpaUUhTdmNDMzQ0R0R0aUphV2xBT3Jrc0tGbnNqcjhva1Vjb3M5SlZIakR5T2ZsTnk2b1NUYUY1RkQ1ZkRjeTc4UXNSWEZxWGdqaENrSDMxUEo4ZGZza3orbjFqTGszLzg4M0pMSEFxZmtGRllBQlRXYlQwSUY0WmVGbTFlWGoySjNadmVackNQODNvWUxWUmhNWEYzUG9BQlBUL2FReTFRblNuY1VxOTdXU3dkUi9aZ0M2RzF4ZC9ydUNWdW9hTU5GZEhBa012TjJaMzQzZk85dXF5Rkhoa1ZxS0VPbjNnWDVoZHlyMStFRXF0RlJZditjelZvUDcvSVdPcFQzTWxYQmZjNHJXT1dEVVJ4dnJmR1VraW1yWlNTZ09KN3pWU214aXFqRkpqTzVnR3M1VUMzTVBHcE5DTmZUVXM3dXlIbVp6b0Y0RjFyYmNyYk4zU2NDaVRvbi8rTkl0R1VtZ2huUjVxK2lUK0NIbVp6SHA2R1g2Vm92enpZaTQ2VVFYNHhkQnJIU0VmUGhuN2M3SXZKSTVXeGZEaEpFU1prZlV4VUp6S2JVVFRCVWUyUUV1eEVkL3Q3M1h6VFAvVXU0ZlFhK05UbWo2dTJBZmVRNFRJM0NjbktacEhWbks0OWszRXpQdWFKYndIbmFJOUFNL2JHNVluODFpOFJxMHNSUVRMRHlYNlV1RVZLVlBMNTZuaGx6aXJCTCtlV3NVd0JYQitjN2xrd21CWmFHL0x2VEsyRGVmaTdnM1ZaY25GUmNGZnNmWGFyRXBqVVJuNktSMGxUSWFPb0RtZDJVb3p4UXh0RVI3NnpzeGF6KzdFelZ1ZWdMVnBDRXUydElnTkNEYUNJOWp4R1YvNGFQMjFYeXhtSEdzZjdJYlZyN3JiaTVaT3FzZk9Lb0ZUYVBvSTIvMDVUM0htZlVBcVZsSU95VzFLaStBUFpBY2tVanpaOUlaVDhReWpvUE1YT2N4NC9jS012enJSeVFHd0hlRW9GaWFGWmhWZkF3UWQzUDJ0N0c5bDR1aklBVXZjRWttOVYxUkJtOHpGQlovRitKY0p1Qk00bEtiWVNldnZvbXhDZm1ZSnk2dzRXSWhNYnczYUw2ZjVDMU51WFQ0UVZUSGg4eUpOc2NwM1YrY2I1MHB5R1FxM3dhbEJPV2JQZTNkNjBTUWhxQlM4bWo1azNVcnFXT05YcklMQU5xeDBBQTZpT2pZRXdjZlhQZ3VQSW9kZlF2d3Q2S3NhQXRNczFpZTltbXVkL0pINFlVN0hxWkZFMUVuSTBsTXoxTW8xMlQ4MGVZdjExYk9PUGxLYXU4SHFjOGpmdTJKS1c0azFlcVR6QTN5eVhLZVhFbmVZK2RSbVVLRGsxRkt2dWh4K3g4SXVReE1DaXMyR3ZORnJMVHZVVWhPNDZqYjE0bHpiUnI5SWo4LzJlSG16U01vZFdJQVB0NGcvV2w0U0ZtYnZXY3J5N2drVGhqdmMrNHVkbHhmQ0pRbDVia2NsWFh0WHBVUlR2K1M4ejNYZWd4Y3p0SFVhT0hYWXBscjRnaEhUWnJYeUJpZ2c1ekQzMXdHVkNpV1ZjNHFobFljL250WXFZQ1ZkV2xXYlRqUlN1alFMemNSSkEzU1RsOWxlbWJ2UHIyV3BsM3pIeURBaktuZU5uOTFkUmxQQnZrR2tpdTVlTW5GOFR3VVdrUUdtTTgyLys2MHhZODBEWmtIVWlYMUdwcnhLREVEM2ozQWdObkJ4M21pVU93bjZZWVhCY0Z5TzZDVmxIU0NtZTVyNnVCRktqOHcrWmxudytJbUdZK0VoRmVLMEZnUGcxb0hzZFB1S0JTV3dGV2dkczdHN3Z5MmZyQUdvMDVJdkxFLzlFMUJzYjBTb1ZJby9VZkxWaU9VdkdNUEhwMDkrZythTHI2N253cjdKNnZZMEkrQ2FIL0I1VzY4cDhJdG1zSzRoLzJ3MGlzeVNCRTFheEhhWnQyUEszYWpVVGhRS1hBbWRNK1JKUmN3ZElFL0xLejRJSzFQMzRsTk1MVU5zdnlyYzRKd2JramYrL3dqVmhxNXhWbXZ6bDlCdDFSNWlXdWdIQVJQY2dHZ3U3aGNER2pXdmUxc3F2MUZhdFdhRW14MDJ4d2VQTFQ5eFNhaUt2d2xLNE56a294ejhtMEttdEszLzVOdTRuVi9kQ3ZtRUZhMmN0c0haYm53cVhWcjFLWVVzSWVyM203RlBoaklLaDRKUkhHdUxtVDMvNkNCTUFUNFJLSnJqbDZrMUJyWkFoRnk4dnlWL0tsU2U5OGFuZE91ci9wMzNTelNQZS96SEdNNTF6bFdMNkliTlB2TUVDU2ZVdll3RHhoUEwvb3pCQlM2WTdWQUxMS3U0WUdTQkl6YS9oZnlDS2NQNG1vZnpFTjdxQTRFQzVuWHlUVDNmdEhKMDFqZEVYdWwxTzkxeUhmenA0NVU3MExkVWl5R2w2Wno5KzgxUWZxUFJKeGRyTmhYNVRMYmpNWnpHV2MwVUNKZkpxU0JrSFhQNWpGc1NZaFZ5SE1TWXVUdmFrWHdhaGhtTG4zNVc0ZVM0dUpqRlQ0V1ppdUs4OFVWS2lOeVRQQ2FVc3A1dWdYS0VkTEgrQk9xQVFSYmtoVlFydmQ1MGVUU294NDNCY2xmMGRxc1FWSG9tTWV1Z3BLbjBaTnlqVHNtWFhCZmN1dWtoNTZhUU9KdkF0SjZZRFl2VGxwT0VHRWNKbmlSWGIzQ05IbmNZYmtjaTdRSlVPVjFvelp6dm5NaUpJeTE1eS9lMkhHYm4vRStqZHY2UGRBWmtsUHoraWR1dmdXMVVTQVpjckZ0cXdJTWJOOVlEYmZ6cVFyNS9qdk5vQmc1VEZ5NEloSkZ1bTFISkNSYjRFMWJWamQ5M3dmYlVsZ3pHR1ZhWlJqcytvRGdydGc2OCtOdkp0SnFBYlFTci9TN3Z5Lzg1ODlBaXVtcmswaTBrTjlITnF3ZE43Q2lLdE5yRXFqT2p6SlA4R2NIR2dDUG0xNDVFZFdNUWFUL3YrUGF0bEwvTGp3TCs2Y0tMZ2lPdnlOalRHMFY2MEU5eHloMFNhRzNtSzN5YlMwbUJCV0x3QUk3QVgyVFhOS1F4eE5raXJrdDJGREhKWFlPU1IvVTg5MGsvNnVuZTN4bzhvenJSb3F3VzBVUnFEYWdqRzJ0L2NHRFpKbzBnZlN6aFlaOHdYNTg0UVEzV2dKM29tcS9yQXVWQllvd005SmMwYkwyRVNCQ25peklSYlNzTzY2SGEvRS8rSGdQK2oyK1hCSVhJQ01NQmpsU0V6QlVUODJLZGU4Um84Ti9vV1BQbCtlY1VlWHRkUTlTRjd6eURhb0RFNEhPamxnVUhiV3ZJSnpsTG5WZDFYZWtKTEhvL1JkT2NLZjJ1US8xalhNWnA2WWloaWlIS3JiN2JyREFGQmd1MzlldzlpQ2t4K2QzVW1RUSswM1hrZVgwT05hWnVGNll5eEY4Y2YxN21xaFBOVlFudXgrWEJJbmpsWmRMLzNJNWFXNEx6S1BDc3d0dEI0b1pwQzgzbnNFaE53WVRQZHA5cmJndTlOZ2RlbzlITkhBTFVJNnRJNGRRNk9vWjUyVEp6QzBKWnVqUUl6bWxiRG0yUlFKdEIxK29VcVl1YldzUXVndllNYitjMXYya1BBaExKb2hMK2Jzd0J2NHpRVzh1REltZ3pUenA0UWtCSitjM010L2ErRjU4OS9yTEVFU1dqTWNpTTFJRXpoL1JjK2hReEJiNE9pcEpaU0xPZHQyQWpVWFZLbFJyT0IveDdYRXNVMnpFc2ZrSDloZWJ0RzFZdlh4UCtKNzREK3N6WVJpQ3QrUEdvQzJaVnBaT2FJaDBGUExXbE5KeURsTzQwZEdiUEIyMUVRSE0xaE9GLzdLOTdoelQ5TTRYOVpmT01xOUZHV0wxRkNBVkcrTWsyMVhveUIrL2lFUEdHbFE3WTY3ZjJBb1M2TnB0d3dLOVUydXRma3BrL2trcGpPQTltczBQc0FsMk12L25WV2lHTG5JWjNscjJNeEI1NkRnQ1lZMlQyaVFUdFRkckNkalJ3MTQ5NkgxZUpvWDA3ZmNmai9yRG5lNFJPaWNMRDd4ek5IKzhpNkpJZDdHdnhUQmFiK2I4R0NIVGp1TStROTlpMDEvTHZObVR6a2FqQ1VzZDJlRUhTaUh0d0xTaU4zMk1WYjVNekV1TFEwT29qVlgxOU00c2xzRTdJVFFmR0R0RU5iTGFpKzNvUlErSkQwRGpveVQzUUh0YyttVFA5aXEzQWNid1VGY2xjVk9GMTFUSWdaaFpVS0FjT2JHNFVCSWtPTWFnTnZFeUxDSTAyRlpqbTRRZWhmcEdxeExQYmpHbWNIb212MnZrdzZuSFRDYWNLampKZ2RyVi9udTRPYzVPQStSYWZjK1ZacWM2SWVUc05RZ1EzTUpFWmpwa2gwa3FTSU96VkQvYnJEMUFrejVEZENnSnViMjlWMW5zOUNlc3d4c3VOUzBlWjRyak5sYTZNdDVhdi9aRWx3RWRQVDJXTGduM3FvcEx2cnJaelR3TGx4SnBncytlbWVMU2QxbUtIb2xuTzVWNDR2b3dIQ3hxWC9ZZTRyV3JXYlJSMHFhb0NVTmROWDJNNmxaeFVZLzFoRy9zaDFmM3kyWDlCS1R6TjBjclRFL2w4NXZWcG1Oek04djRCREFLQzlUY3I3Ym9yaTIvbDBPUUNRZkVrZXAzQUdlcllsVnQ4K3c3R1V5RkRtVEIrQUlEMFgvcFdHTTkrYkU0WlRJdFh3Z3NjNlZhUFhodTM4TENTa3R4UVVGYjh3WnJqK1RISWgzengrOGxKaytZM1FwLzVheGVWdG5pc1h6NERDTFU5Qi9ZaGpaK2o5NXVjc0RsRE9WeC9ybFI1c0pkWmJvdFRYckFObkNGblZjTW1zNkVvblVDTnZ0Y1BRUGpBK3NnWThKcWUzSDdEVzNUaHkxVGJsL01tOXhHU1BYd0dzb0hXM2FFNDcxNnN6MjI0SmFlTEwycnY5anJqRENvbC9uYTJWNUJVR1ZVSlp5NjZQSnk1ZDYzQ2Z4SEdKTEFubWY3L0t2dFNCNjlMY2xvYkdYTktuemJMM090Vlo3eUt4YkFNQW1FYm5vbTlscWs2eU1la2lkUzF2M0NTM0JCL1BraXRWeGs4QzBIeUdPUE9mYVlMSzloL0xLd3gwY3BuL2JMdVIzZjZuMitYSFgrbmd1cmhpbjhpekx0THNTY0hkWE92bzVmWmN4bCtFT0Y4dU9mNm5UVk5QakxHY1lDaklzVjhvU1JaY1g3a0dnUjR1TXp0eWtpTkdCRDNzODlMMUJEdjR2K2RUUXpjbENoN0w3WCthdXp2ZDhXY014QXFIR3hKN1dtMldqTDFZSXg3YWVKc0dqTzV5ZU9oMm9la1BwSEp3MEFRZjRMRDlWMm9vc0I5NmZVcDRCeHhpeWtoOVV2ZzdqT2pibDFQS0VlS1E3TXptNUIxUE83YVdDZ2dOUUZZZlhQRDc1R2xRT2trOW9yT3g2R1hmUmFYL1hENCtDdThQZDd5V0dGNEY0eFRXZDBIeWY2aTFrZHdFb0pOaVc5ZVFPM1htaFd4NHlIL1QyVDlxci9rUjVlRldrdmhjWWZYcXZWeml5ZFlZbzhMaStGQUQzRTl3ZEZPT2Y0SHBxWFArMUYxa2szK0hXZ1Y1ZmZqQ09hMUYyUkRreDJZaHNCdGQ3ZEJGR1NqZUpMOFFVOEU1THBPdkJlUHN2dWlVbWJQQkFMeVROTHo4aFE2TnZpQTE2OUw5WXYxZlR6MzBRd0tMQWxEZHdZakFLR3RGRTRTVDNhSENSd1VvWEFpeHZHTTZzZUsvaGtXZ2dmMVBPMVZGcEQxZFdKSXZsM0hRNUlDc2VIRjM5RDhKUmpHZUdabHZnV05kUks3Q3BMOFBIUXlqd2pTTmo0cTdSSU0xajFjWTJNMXNwanJsRC9KUEZSWDhwVG15Q3I4dW1zRnZ1NmV3MkNOcTV1UlBEWUZVRXFyYkc1S3U2OTdiZFVld3VBcHJQTUNzMDhPZnlwMGhyMnlybUdMM3JhM2RWNVJZN2xibHFoVUlYRU96R1lpWEZabmhHS3B1UEhpdkFxZGNZZldQMVEyTGRDOCtFSTdscHZ1UDBrRHUybDF5RmU0T2w4NUxhLy9IbGxlUFZHQTcweno5MkI0Q0s0REVxY3JUYnBzTGhrVk45VW9GYVpKSTYrejJDSVBUdElaenFWNjYrMkhMU0p0ZDQxTVlFb3YyMWJ2Qlp5YXhtb1Z1L3B1ZWVVQUZxUWVHN2gvRGdYR096UkZsWEc3ck9NR1JSelpRNDJTdVBUWmtTd1QraC9rakY1cDNNSkpWR3Q3MDNLZjhGNzd2NGdhOU1TOTBlNzBXcklJTXcxYkMzcFhjWURYdEF2RGRzeXBrcjN4M3pzSkIxRlhoaGlIbVBZTkplR0xFd3FZaWRRbTZJTmtGa3ozM0JFSkFGOUlweFI2S0x5WHFMb29lNUl1b25tOXhDamc0N3BjUFdRc3M0UHNBbXArRW45SnN6ZTRFRzd6RkNXNzR5UkpoT003SzZJemhGYlZ3eXF5K3ZsRXRBT0ZYZHVRS0F4TWhGN1Y2VUZvN0NZU3BiRjMrRHhhQ0owM2xyV3o1cVc0NEg3bGQ1Qlg0N1FpS1MyaEFXYVZtWlNjTGYxZUFyb0w5c3E4QmVtaWhNVnk1WjNOMDl6T0NiSjMzdC83K0syczMrSkFXV0UyNDZOV0wzbmZHdVV0c1ZlTFFMNXdsMlFoYUpTVU1tWndjc0JHWXBza1hjV1dndTJtZlVQNFRsZlZjT2RlTnhzNzNBUFRjSThWMlE0NXlvaUNCSnkwUjZoQ1BKT2UrdWl5RG9IRGw2TUpQWTllMWRvS1VwTko1dzhWbWlHQmZ4dCsvZlVGNEVxOWJuUDJhVXZpOFMwL25nbWRGWExHZjBjQlBDZFlqRGg0Y3lkUE5LTXdDbUw0WmJreDlDSXZSOTVHYmJQcDlCZEZEY0FMTFl1NjZKMWdrdk5oT1I3VUFJdVJ5NHJtU3FKbU92T0V0QlVmRHc2dXdXVGtHNyszSTh1bi9CWTJ5QzByYVl6UlFtRHgwQ3J2VVhMVndFN1RUSjZIZjN0T000ZFNtSmNMUUNROWxkQWNrQSt4YlZKVFZVMTUvSjRvQXhpTVdKR3JXZVpVOEtYNkpoNnd5am1kbmIwb2VlMG9QUVZpSXVNbG9iNW1zc0pmVklHWmJmckxqYVNZMUtlV3UrYnFwTFVMVmNJVDF1OW1VVktQb3FxUzVpMFBpa2pKL0E1bVIyaCsxbWRDeHV1WnRhWmp3TFUvTkJud24rdWdBdFhHa2tnM2U0N2F6SklmSVFnVUhuZy9Ha0duKzBFajN6Q2xoNC9YVW9yaDZYdjFFWWU5Q1Y5eXhzZVY2NjRHNWdUZTRlN2R5T2pmRXN0cGpiQVpGRHltT2lDcmVBWDZZY2FXbmlkZk1URHFJYXpwNGRrSytneWJEcTM2cDdPMFYyREJPS0hIb0tvYzJQQnpoSjFRa1UzTEZIZDNGR2ZUb25YeWJvL29TTkIzN2dGNUlsdVMxUk5wL1Vpeko3KzYxOElzWU8raHU1Z05mTmtXRDFyQmkvWXI2bGxFSW1KTTB6ZDByNXFTaTBNbXhyNnJkMURwR1dpb1lSZTFKTEhMWC83eVlwQ3plalZocGJyakFjUFlHVlpmUTk3QkVGc0xZbDAzY0pJcGlYY2JDYmV5M3QvRjQwTGZkQVlhYlVKQjFrNWo3OFhYV3k0T0RkSlArWU4yZFhrTXRXVllkMWVJTXYyQVlTSFFyQjJhZWt2T25XK2hkT3dhdG0yQWp4NUFHODNsVXJwMEZxZjdUYjh1WkN4NldleUFUekxCOEpDM0xBczNwcHpHNERtZXZVNW9ETVBndjQ4eDhlQ3B0MXJXam8xUk1qcDdSa2tTTXR6ZnE0Z3FRL01FNGtaNzNLTGFENThPQ1B6NUFJeHhleGRCazk3MGpxVzJwK1lmSllTOXBMbjlxeVBVMm5QZytPTW9icXhMNUw1bjd3OXB1WnZXUEFQWW1FYlNzTUFJWHdyUHF5ZElEYVYrYytZRHg4NkxxZ0JrZEhXLzJUMXVWS0ptcjZFUWdxS3ZXakdRaVhnNWtSdXVmb3cwdEpPVEM5K0dic3FSNGVtZGUwbTdrLy90WGx2ZzZjVkVHNjlVNmU1MEFBZUpOSlFjU3NyVWFUSG1uMmxsQlJFaEtEUFFXdnNmaWpFcWF2ck1mdmFxaGxibVh2V0VoS1hzZzgzMmE5QTZxSW0xREZiOGZiTHpMNW5pOUJ6THpSVUV4TGNDT2FwRWRzOFgvckF1bTQxNlFSaVFUNWVFbmg4YzRlNTQyOFpMZnhiRXpwUGxtTEgxSTBtT3BPSHl3QmxBRVBoRDd3bXllZldpa1ZOcHJtdVJTc042RjJwVndqbUoraFo4ZURySzZ1bWhTQmFEV3d3NE5odUhEU0JkaHNEYWJoektUeFhFbkVoTGdGbExyR0ZMbmxlYSswcWh3VFFsZDZISnQ2eE4yS283SkFQVDl4d2hDNXJYL0dYQ0FqRm8xeStoTmh6b0h6RWFENndKNUQrTzRtd2dySUlqd3pUTDFVYlc4cUtlQSszV0tjVmZuU3dlWkxhbWdnanVXOHZwUm9kTTdoSXlFbzNPUERNdnk0T2JyQ0k3SDFMTXBFK3ROdER0aVFCR09BWktxdk1WU05CVWtXOVI1NDFWVlNXNkNYS01MVGNLT3o2WldybG1OQ0NJNzZ4OTR0bWdEYzJYVlJJYWtyWDl5WEVlV1hUbytVN0xpKzMyUGhvaUpjZVNTK3VKdHJYSk9CbVhVZllVMzJXdEVCK20xK3lFM0JhMUsxQUIzdlRiOXhHWm1tTHY1cXJHT0hXS2VSS2oveERpTzRxR0xOcEJEMnh5amMwOWFidjBQOVhKdVZlUW56aGdISlRZREhnYmxUSWdVaFY5eDlZY0RocXJwMktEVXY5YXVKRStHb0xwWVUySFBOYW9ZMzgxeHZBMmN4aUZraEcyYUlYZ1YxT1MyL3lGZ2Z5VlloMG51dy9KTGdINW5jWUpkaVYwZnFKNmhyYjhsM3BiRUpGeGE5UVJndVQ3ek8rQk5hRm8wL3VhK2VtVXhEMFBvN3F4YnhWMG5taXZHaE85OFVuUjNlbW4xYk54aTZHRC8zdTkyU04vL0tXcE9QZ0w0VEdMc0RDZ1V3MU1tKzh3cmdrdnY2clBBZDYxcFRscFVkWDNDdFdEWXVPRHBQNnkzTXRCbVRGeG9oaW4reEYxYWFLWlpPRDBNYVpwcnFjcnNRaC9kZ3Z3dzRFcW92aDlULzB2bmZHQXJ1NkhuODVnUnNGbE1ReXluQnVHM1gwaDByQlp6S1lIWk1VYUxHdm1GODBQYWRYWUxrdDRxMHJ2d2pCV1h1dzVMd3NyUzRBTXk3clFHZDgyQTdBMWNHRmxZL0ZnengyVUhaekozWFR4NVRZeEVtVTVFeDBVa1dvcDFGRjY2MjRRSndaSDluQmRQWUxhL2dzNG5yTkQvOEppbk9wOWRKMCtOZGViRW4xUlZCOHdQRm1GQWxqUm5oZDdYSHByZFY1VGVRN2Ywd0YxcnluUTlGYk5zMXBKT1FyeGR4dTdWV2pvZnNiUS9iMlBqanUzQ3Jvb3hSR056TG5Sd29Zb3M3T0xMRHJoNVhKSzZ0aFk2ditTVUNyeHQvdFdjVG5wSWpMamM5UlBWNFNVU1NXMktxSlJpZVpuOVIxdUlPUTFYTnFONVJTaUxCQ08wejEzUjdyWmtUY0tWYksweXdGWldzWlpIdi9qWGZhM1FvNXV1b3I2WXVwUXo4TnlpM3gvRmtCT1BhWmh4NzBCTVlqZzgySEQvNnFEMDJaS09NUmF5ZWgzV2tuWUtlRmQ4SVhlOU9adlNGUnVINDFvRnFORnIrTDd1ZkFQMHdtWWlCdVpCWW5IbUFsRjVqVXFmYVAwNkgzRjRuNExxcjRzK09OZXZqV3EyMUlGVmZDVDd5RDQramFpQTBHTER1Mlp0dmU1VHNkNVJHWjRIY25KK3hBa1N0OEZ2WENCRStqdlpIYmhsRktYMkdPaHdmUkczQUxUdW9YWHFzcUQwdmZGZTkvaTdUZVc3eStRSUxFYVZzUEhwWHlqcEpRcjZhZVRVY2J5bGlVd0IrLy9IUXVEUEpsV1RZUE1pRHNJVktja0c5all6NzRBYjBUd3Y0Mm1rN1ZVTTVXYnVMVGloZkZiZE1HQk9Db2lBV1ppYWNwSHNTNytpeUVTeDZQRDZzRk5WdVhyM3hNTDc0NlgvU2NubmY5QXlsemZTdTg3c0g1eWR4UHIvK3lqbGRKNlZ4c2trdEVZNFQzRGs3RGtuRGg2ZHZseS8yWWFiRGVQZkd5YWw5TzNqa1BHWThIWE5zTUE2dG5QdWQ3ZlJManF3WjN0MGFkVG9ocGJJZmllU2JKbTVIR0dnSzd3Y2IwUFZmeWlham5wWTVUSnB2dk12dG1aZ2d1eU1Vd3ZmV3QxMWpRckpVblZIZVFDdWFpc05LRG82WmplSnlBeWpxTi9qVkQyMWxQdnBTeEs4ZHdubW5uQTZZNHZOSWJFZzloVWtlSEdNWURkMVRhd01kUi9rS2xUc2NJKzZVUFJjOXIyOG1JREZKeS92WXk1Nko1cHE2VXFHWmI5NzdXbitldDJaNmRUQjdWbkluMVU3ODB6bXpkT2k1cEZSZEdlNWNiV2hXZ1hzeXQvb0g2ODdxK1NRSmRsOVQ2MDFFaGNGMTJOTmtRMURnMW1LWCs4enRFc2JwOGgwaTY4N0p1QWJubkJleHY2L3hXTkl6ZEpVcSt2Y3JZaGFrY0psaVVkdENBemVhdDQ4M3FSemxiWjI3V294ZHp5b1l3N2VqK2pZTXBkSVdDQjhoVkVvS0xHN3ZkU3F0czhRTldObTVlZzBZNlptZ1NoYWI1SHlkd29rM3lyQ3BmWVQzSWZvRDc2cW9IcW4zYm1lZ1ZVeHZhWm1KR1NkTit2WGZ3bEU0RjdUN0x6UGp2b0cwQ3h2VmR1UWFnbDJXMmpncGQ2S2hUcUFVaXZSK0sxYUVPWjFQSmlUU1Izd2dCeldnSGRCRGtvNXRXMzd4TTZrQWNJemFFUlJEZ2FwRnRvSU1XRllaOFVyWmVnekcxQ0NsNE43TUR1UmQrK1EwUnV4SnNiQituMHNibkthcFc3MXo3alZaKzFPbEk2NEQ5aHR5dmEyRXhjc1lBRHc0NzlMYWlxYm1hOXJmWW5zMWxhVGxrWVdjNSs0allEUHdleHN3aXkvUXFwbXpMcWxqK1JVbWFSL1ljY0QxcG1xcXFsbFBGYnAzVXU1a1pOSHVNMi95QWYwTCsvVjZaZSsvelJ5SkpnWSs2UEY4SW1qeklsZDAxM2pnQTRheXJZWEN0VWJrWVViSmZFaDdUZVdzb3F5MnVhOVU3Y3lXVUFrUVkxd0hMM0lod09Ta0lnc2h1U1FTSmtGVSt1aTBKcEhxZ1pvN3ZUd0tvc3lXZFZ4ZHMxSmdkU3ZiZmpCTlFqZTVmcUdRQWRUSFNWSDErYjI0R3dDMG96SzN4cXNOUGhpRXVGUDNpVGJZN3JWUEd6WXFFeFMxSGhtc1IydHNNUHIrSTZvUXlnU3dqNmFjL2pXUnhDVUFDWTdJUUlNcmE2ekNpcXBTYndSUjQ3SUVzbkhpWnkxZWp6UHQ5SVpxTHRBcU1EZm11ajV3OW5uekVRL3U4cVZwd1JpVmJXRjVNS2RVNmpsZnNqQ0F3c3d1UGdhZit1UGhsQ1gyTkpKcWc0dzVvclNnY245bW5GNTRqdVM0NnB0VUErTTBHV2NVKzc5NVBUY1ZMS3g2T0w2d2ZqaFQ1eG9aWjYzKzJaU1pSMElhaHFzdzNiWmh1TkQ3U2lZSlJZZFNzZFNheDU5ZGpGbk5uWFF4K0hISDByekpKamFpT0NET1BlQ21nWW5KZ0p3anB4T01IUlpQMWY2bGR3UkxWQjZNd3pYQzVlblBaaGpiYWdqelVmYVJmWFJpSmpGNU9ndjNPQ2JKSXh3VkxVc2NSdi9uSHora205aTBaRXFHeE8vYi9MLzZoZEM4K2JpZWFaM1dJOUdzemdxbGpsTzE4K0VGd3NpYkNDS0gyUS9DdU9iYVYxUERES3BvWDhaYTlLTzlWV25JUXF0MGxRcTMrL2h6c3pkS2hYNERZMkkxYWUza0YvODU5Q1VUVXdBc25HNkRjaks0NDZDRmt1REJyMTZxYlJiMllrSmVVUHlZYjRvMTVwbXBUdit0QmthTkh4eFFLRmR2d29JR0YvQzNNdDdCQnQ2am9PYmpIdXY0UjV1ekpVL3IrS1RMN3Q2UThqcEg2OUJaWnUxVG1HbitOOGNHeFljdG9JS25uMkNyaFNpNjFTSWhLZVhGYW90RGZla0ttbWJMeC9kVlNSeGJWUllLbHlIYkhvNklXRDh6WThqYmN6OXNSdEE4NVZwVlQ3eWQyazNrekUxcVFSRUY1azN6ejhMRXljcDRLamlnY1hoTEZrK3o4UGM0NjdSVGtpTmRhT2JQYUVDZkhxWGpZTlhsWXY5bEVJQnNnTFQ2bHN3WDhGSVdndXZXb1oxMFdJemVxSVRRY05zMnJ1cVhGZjJPenYvQ0Mwc01Odkt2d2dzeW9FVzBjL3g0b0NBRjlpOWRxaFRnQkpqbnErM25neS8xRmxxTVhxUTI4L08xdkQ5eERNa0ZRRFRMZ0pRR0xxamFHTjRpSmtyRGJiMmtoTDlzOWRDYk9Za2J5VFZ4b1hiSitWeUNKNmFwZit1Sy9JVEZoblFKY2trZElMWkxRWUlPRFVIOGc4MG9uYWU2V2JPY1NlaFNjT1dwR3NZd01Pa1N1SElBSUEvOVNLZytERWxnVnViUVBQRnNuaU1XL0gvUzlnTmhaTWdOVUl0akpHempkTFFHOFRZYWt6anhlSGlJdUtsczUrZnNWTUQ0aEg4c2twUUZwclJZM2hQams5R2ZRUzlja2M3SXpQRVFnM2diNXVFVUZMMUEzbDJHb015VjV2L28zNnZ0Z3A1R3FKMTY5SWwzOGVGS3hTRWV2ZklteWQwTU9ka1dpWmExV1dPNi9PWkdrWG84YkZxZkZraXR5R2VCQmJvV3pSYU4rc3l3SFJib205bUR5T3JZcGZDYy92N2NyNmJMR3pCVnFWMkNkaUtveWRsZ0JLRkZsMEp6NW55Wjl0dmwwUjJJN2NENDB1NFo3Mkc4N2FKTzM0OXRnSlNGdnErd3NpUUUzS3h4SW1ZOVZ6QXQ3TXBrdTRvR2pUV0diaTNxSUU4YkUzSDBPN1BPT0x6M3g2WkVYcXh1cGtUK3AvK0o0Z01neTMvS3pKak5RTDEyV05vWkpzSFkxcFh1bThmRHBoSndDaTJjVHMxelJodlhlVEZkQXRrL0xUa21mQjlxR0VCa2ZJQ3k5cjJJUFBFRDhKcUF2NHBwS3dIeU5WTDAxZUNIbmJ6bTE1b2JXc0xjQTloTjl6K1IzSEdUMVUrYlh2L3RiVWlwLzN3RHVrWlRrMVA4blhKMFVCSm1EYkxuVUVlTjNoV1IxaitSWUZ3bml1by91S3dPTFQ5L0NBQ1IvbW1OZE50Z3JqUHNjTGt3NzczdzFtVVd4bjdqYktlYWlXWWNKd3B2VjdCOUE4Z3phYlZyR2FTU2xVUldQYnBjMmgxZGJURlUxc0xQVFpaMGhvd2hUdmkzRzRrQi96T1BSZWRMTVdtRFk4Tnh1WVBHbE9sdGlqNUNxbDIxU3ZLVVBCV1I4VUhCbjIxd2lBcFFiUWp2NzY1OUJtZkZhbHpiRW1lYWRYRXI5KzhLcy9BTzkya0FYa1hzSFVHSGE4R2RwV2o1Ny9pclhKZEF1d2w2WWdwczd0ZzU2SkQ3Z1ZFOHRUMkhLQjFhRTFiVElpV0RFSHlwYjlnTWg3dDRFNVdRaFgwTVV6N21jbi9MdllUYUt2ZUdmU1hJWXJXSmFlNzJ6ME1qQ2sxUW8wb0x0MVhuakwyamIySCtuazlPZU5paWVKVm5yNC9KTXVCOXNobmNVU05yVXZaQ25Ea0R3S3VEdGI0cmVldmJEa3N6dTE5dkNoL1NEYmhRZjRVRWNOdlg2UXFtRXlScmFLK3o3YTZ2eSs0Y3I5cjBmZ2ZyY1ZhclVDTm4wbHFNSjJ6NzkvZVlpTHpSVUlQU3BhbnRXUmtnMTVDM0Y5Z0NDK3I5M1EwS3F1eUJxazdOWXNydnpTT1JrMFpPWTd6TkZRak81M3B3Y0hFb0VQTkU5WkRvOVg2WkMybnh0ZGU4Vks4Z0poYXVmcHBDNjFtQnp4M2JvRTR2WXVEMEZmdm9CWVlIdWZJRHltNEVwejEyOUtwVzRzUVJ3OEpjcC90bEN0R0grMEZDY1Zza1E2OEJKQ1ppc1EwTU1wdFhSQXR6Wm11Y2hCT3Vldk41bkJSKyt5eHR5emlUV0pVUDZMSjMxam55dm55SFBoTzVGcktRYVdKS3JlRENDaStVbm52ZEpmVGxSY3lQQmlaMEtxbzlEQ1NYTkorK2ZCNG5HYzRPd3MrRWNCa2I1a21oT1VoRG5TTDhPUkErTExwUTRRNFd3TGFZazNBbk5heHJwZ3pIeGVtUVA0blZRbVA1a1EzMHJPQ3dLTlJnU2NQbjlmeFpFUlY2QmdLWVJPR3dwNFdEVUlkc2tOeDZGMy85TXlQTkdaWHd2OWtkaTlGTXpBUDRDVnc3ell4SWpieG1OYktlNVppTk84dkhhdWxxbEJnZGtleitudXBoUGw0ckxRZFIyOWxvNW83b0xJdGY0OHRqdFJOMXNESi9UMk9zNm5zQlNKbGM1MEdDdHUyR3FqcmVvWXN1VTlabzdtVU55VDhtL1NLQ3JTZjMrV2FtaXExZFU1OXRTeGdKSERja0NWWGFBMXlhTjZSQmUxR2p6Y3JwRklTVWpUbk05RmE3M2NCakJEaFl1VUpQc1lPRnV1aUxsVURRNXREWW9INFQrWjFaWlpaY2JNaTZVZEhRSHdMeUhEd2lFTzdjSi82ckJkUXRpNklQTU80SVNpb2x1blJ6ajdhNGlnTldiTGxad1JNYmFiTWFJVEVURmVCcFlnZmJMQWwxT1Q0ZnIrSUdMaFFGWWZydEhjMkJJK3ZxNGNMd1AzMi8ybGRwVHRXbnR2YkFUR2Z3SkJTWGo5UWJ4M2hsRnBIYXk2UjlqM0JlcjZRbjVNeUtUNG0rMys2SVZBM3pzWUJ1bzFUYUlyU2o2VWtSMlJhUkZZRGVtVHdsYTVNeS80SFI4a0ViY0IvbmN5NnlHWjNYVWcyYjQrT0lGS1Z4dVQrazJnbnZTb0d3N0YrSEl6QldHQVVyV1NwaTR6YlpYWDhPamJBTUZZVnNvd2oxeU81dzhEbk5yMno4NnhrbjkvbE5udkNBcVVKMW9mRkxkZG4rcmtyaGUxUUJVOHVJbDhJenYwa2kycVVFeVM0S1hhOVBGQlBzWG02YUtnTWlWQnlxeG5KRC9Lb0ZlUHk5MlhvZ0VhYm10UzVyMVN5MnVNdXVYOWQzeW9sbW5QNXl1THRuRmtzZXQ3VWU2aDJqSUFSUmlOeTJFZmxaVTN4MjFXS000WU1XM1Rka0tOSkJyNXlKbFdnRDVNWm9BaitVNFV6dFd2MW1hUmhOcTlxV1JjVk9MUmd1MG8rL0xPNWk5RUdQcUVOWEdNaE9UbXpNRXh3NlJNTnFjOEpXTzRMYzdXbFpuRnlTaEFYUUQ1WDRvbTJaZFl6ZXJmNWlHSy9qVUxxeUVWQWRLYWY3dGVLNDRUVjZOTmlEajlXT01BRkY4c28xRDRtTm84cmtVZ0g3RzhaNFNQN3dYbW02Zm9xNFFRUnBFM0F2bmxtRTlmQVZ2T0NTMVpjekttUGFYQi9ZcmR3Y2V5MDYwSWNrUVpwWHBxUGxnbzc4TVBoZ2xxbjl4QjVFVW93UXlKd1BZMlBZQTIvMG5LZVk4aDRtd0Z5OE1iSm5rOGcxQVVlRXcramZkWGFwMUpPR2xua0NSOS94ZGxGL2JBR1dlRUcyUG4yYVAyaWFJeWh2ZjcyN0VKY3k1ZVcyRThHc0loU21OK2JQc2lmbVByTWdVZEVvS1gzREg0M25wQnYrRjJLQjkrK3VsQlFNbVByZ0xkbjRKWUJiN2loY1UzL25QNjZvK3ZLS0JZb0VKUVZ2eDdIRHNsZURUclY2SEtPZWRZQWlia3BMRCs3WmlzQmJUS0xEdFZBNkQ3YWxVckhpY2RoL0MwQ3l2bE4rbEtaQVhsY0NLRU0zcHdWc1JISGoxY1dzM2lkeVg3bWFKTWVaOXlpdjJGZWF3Q2lMeDgrRmdpRnU2eGRDODd3TGptMmliZ0xIeksxOFVVdXdMTWpnOFZGQVNIRzdSOEVWajk3ZnZmWGh1N3hIRlVTK1hTR2lUdGdrNXFnbXYyS0E4UmI1UUtsVG1zRGFldTVob3B4bGdWN0M4YnVNdEUrK0xYM0ovRnpOMjMwSlRRTzhkZThsMm5sYWJ4VnpoUWVxOUdYdmpYbUxSdTNCRGFqTjVpUzZMTGhESFpEaHNsbDA3OXpkalo1UG5uZEpocW1heDRWbTNRRk43SXlTSDVsZG8xcHNaNzZjR3J1bm5Xa1l1aXFXMVlVODhRRk9GYkx4Y0V3WjlNN0o0RkVYbmMxQndaTEk2STlMcFZIMGhSQnY2cjF1YnVWQU1Ic0d5ZGVtOVZXQ3NvclN2OG1CYlBlbm5qMktFQjNXS1c4U0o3YlZhMW5NVGZ5Zk0ydklob0MzVDZzcXJxWmxSd0ZsdHVnU0t6SG9tSmFTdHNtdnNBajBjNjJ6ZmpTTWh2NnNsR0hyWUZCQ1BkMXdWd3R4ZnpJdHJFaCs3MlJlajBOZzNOTmN2b3dzZmhTUUdXOUg2MXFNK0RyUlhtck51dnNwdXNKbFQ1ejcxRDRZNzVvK3Izc0NSbFBDTDN5OFN0WlFFQ2IzTUF4ZjlveTdhMWRKODc3V3A0S0hubTZOdnRqVVhvVkhhUFZyWFFQZXNXNlpxUHhWVzhtUStKK25OckZlUTR2R0VTNWpNZVJDeUY3MDdPZkRob0lqcHl4RUNHMFJTS1B2blhmcWpqNVZRdFNxT1lPeWI4QXBYNG8zS2VmN1l0TW1MV1F3cDZ4YjhUSHpEcDNFTmM2M2RlbUNTUzFXb3V5SXZTVmRENlpiR0NibjJhZ2ErTmpyNzFQSjZ4czRQZWR5WDZYSVpBRDNETUlJZ2NWRDMybXIyb0F3REMvM0t3R0tBK0dIRGIySTZjM2JGM0RzYXFyQjR5clhtR01JMy9XTU55VU5XNVZ2LzRWb1Zqa09xa3lCTnNSSWk1bjg0a3BVMG1RZUMrNmZka2E0SmZMYmlhN3Nsa2ZqZ1lVREd2NTlwcG5tUSt2VHRHQjBKb0NJNG5HMUhMVXMwM2RkbkZLaHFMVFYvVEJQZU85U1MxWUtESUlVMzlYL05TOWkxRUM4SDZZUzZWakJVcEM3NERWbFhsbFJINnRaTGRKa3Bhei9KM25GNHVsV1ZWVUVnTXJyNjhGV3ZCZHZXL09uWW1Iamo1NGlkekpQblNhOGpIcEw4UkQxRmZUZmo2aUhna2s1bWJXNTRpZ0FCVTJnU0tSamlsR3dWenVMVXZ1aWx1akdZSzdsd3phOEp0UUttMCs0OHBOS2QyQUZvT2JURFZPK0JIQTZ0eVJPR2QyaWV3Wkd6TTlhSUdwcWpYNGNJUmpJcUlCT0ppK3Ric1RRZGFiYkd0em1XS2FEKzFmRGN4OVZuRVdOT0xxU0tTU3JHbWNIWkZyTnVnT1I3ZmJpUVBpM1VwTVJIRUF5RXRaeForcE8rWXZaSWovcyt4RFZUVlVxWTBwajZ3MXZhbVAydmpqTUF4bU5PZitFQTV1UVZvdk43WVl0MHl1NDhrTVdZNlN1SDNlYTJwT21zVmRpWVU2SmdYN3VnSG0vSXlQSWFTclZ2aThsVkVjbncxZ2dIQkdKMXB1SGZpQlV1bDFuQzR3RS9relpSdUxTa3BnNnlNUkJRQkxvaGQ2Q2JNYzN0cWNsN3hnenVaNVIrRU9WTWhsY0xnaE1yZUJmNVA3ajlHOVk1SGhoWm43S1pDR1VUaHlyLzBrVlBlT0VZMW03RkZvYWNmNVJVZElPVjNvUjgxZEZldlh6bkdvdldQLzVxUDFLYkV5RHIyUzJkN0I4MVNwNENIU25OQjhGYzV0ODJ2MDhnRWxaTHhHaEFaWUthNzhjYXQ3QXkyU2RDVkx3aVFjanRNMEM1clNwNS8wRXF0T05CanQ1WmZXdVRGa0FETHFCVmw4M1o1V0FsdmZOMzJ1d2xMVzdHQTFkVlg4am5DWWlvcDFCUzJpZlMvNFZlOGxWUythRW04ckRpNFNDSFVIcjhyblFITGUvVXlsWUhzV2t6Rm1USkcxd2Z1ZmtNOURuOE8xbVkvcm1WcU1Iay9RNXBERnBZTXBIaFRndUppRy9QSzN1bE4rcTJkRnA2YWo1LzYzY1VYY0NCQWxIdUFEUjdMU3gweFRZcUpET2dudUk5cytUNHNaNkhwM25CNnlhMjYyc3BSVE5uT0pSa1ZWdnNNZGdCYlFHR3VwZ3BQV1R2OHExb09odVcrRHJuaU4vODFtQXRxVUJGN0Vrazh2djFQbTl4R3ZIWnBNVSthZXlUTS9NQ25vdkJzbUtYVSs3RlF0VTBqVHl5MVhjbDRCUzdNL2xMMkZEMGUrcEFLQ08wdVlCd3lkRnArWWNraDlzVjdYZ2hJUy9xWGNsb1owZTVYWlNDcDdPb09KQU90TndReG9mYWx5ZFBqdXBhNWJJdnkrVkVJb0l5WjZkdnpRWHBJWUJWTjR4T0Z1a3BJMGdBdFlsQ3JtaFRtQ2xwc0czRG51R011UHRIbjArdzlnaHZjQUc0aGN1UmkrNWxleldUOGRGRUFCREd3K1BEbEh5RmRFcFM0a3kyeFJaalZWZEpFNlB2RzJHT1p1S2Z5VW1MQUNDOWdqWm1MUFFTenQxazF0OUVzWVMwZUhScnF3UnhHWkc4MFJxTVhKSHdpT212RGhYLzBFbkI0K0tKYUZTRmZXNmNlKzVuVXBqNWFmZEdRVXNkckE2Z2R1OTNuVmlFRFdDZEVORjcrZXdCbiszMkFmMXFaZlhaT2NiaDB6OG9ZcXAzMWN3ckFBSmxCVFNTbXhKa1pqb09UOVJXSUlZQTRtZkZCKzZLbk9rM0NmLzBCWEU0ODkvUGlJMEVERUVkbE9qSktSYWN3VEplci9CTy9kZ2pOMEt6aEF6MWFUWTNVWnFzSmlXeVdJRHdQVmtaN2svVm1OWkRmME9qd0NkekVEa2MxaGtaWnhwODk1UnorVFhEam1aQ1VqN0o2WkQ4RmFnN0hDRElmOVB2b2VXR0l0bFo2TUpacUNPWjF3QUQwMHJ1Q0FCVWVaa0NXZ1FYME9Za0Z4dzllU0Q4anNUekU3TUw3UHFFUEFMS253K2hMRSt2ODZMNHZlWGxNWm9nSTNsN1BlTTNmNEo4S3FTK1gvdTlRTjNUTjgxdEFoOW01Mk16eVB4SzRDc3Y1OTQvVzNRV04rZ2txTFA3d2M1SGYydit3aytPVjRnMW9BNjhYT3JwOVo4U2tldUxoM3VxTEhXdEVwblpMbmg5UjJ4NEJYV3VNVHVJSm9VckxLSHplNGxzbGxsR0NzSVdqQitpYVhJdFZEYmZFN05ya3RHZk5HeWZRVy9zalJUejU2T0RUVmJzTXF5L1A5T2NLZE9oRnB3aS9xV3NJcmRkVDhaeHBrSlBMODdGYnZNTkJ3VmV1czdYTzRENVpnMkM0V3ZhY1c4R2ZVQlB5WnBMZ0tMaGxGaXRSVFkzRWNlNHpBckZMTGpSQ0dzNjJWd0YySitWMTR3bXo4YnNuSmhqUjNpSGRvamwrOUVkbEszZXBpZVRHeFp0SnpZbnFoTFJaancwSE0rN2h2UVNkdE10elpvVzBGaE02ZDB2QWNDQUN2Z3BQT09McFZ0SFRoNXIwdFFEZ01lZWxjcU1DOGtKRXZ1SkRMODVIMi9aQzI3am5MWWx2OGIreFd0cWNnU1lEdDFIYTk1NHd4NGVySEk4TjVMbm92aGc3MTU4NCtxWCtkeVBWK0ZtWnBNdmYyY3lLMzhUSXVlckVXeUdEQ1JWdis1TWVnbjQzSzFFdGZvRUtrQ25nTkd5WG15RHZ4dzRZR21OMU9uRUV4bW5rRm5iSmNxakt2b2cySUY1WVZGNkpCcDdWODBxMUpISWExSVM4K1puZlBQZFRvVFZpTWY3Q2I3VnB0ek4yRnFFY2czQ2FrVFFlejVqZ1JNektQcDVJU0ZTdTJOcENVNGg4emM1Sm1yR2R2ZUZCUjBoQ3dxNVhIZm5zOG1DcEdEUllJMWdDRWtLQkZFd1hWcWlHV0NpL29jTmJVdXpXUUxidDBRMVBLdFFvT1VtQXJEaURZaUlRcHFacUJLaFExckV6ckZ1NERXMkVsUzRzZ2w2ejVHN3NjZytkQ3h4K2lKb1ZCREgvRytFUXFFS3hKVXJKMUJqb28xQWJCUDVSNis5cm9UU2FtM0F4MVUvSSszTTlpZUxBc1NrOGtWVnBEQWNNVUdHQ0pPb0x3TzllRWIxODhaM2crS2JiN2dYTnBRYkdpQkZoQnhxcmZHaVh6RnprS0sxZFNkL2pXWlMxaWkrZHR6UjI1dEczbm5Sc2Y1RUsyMXR0U2dwdnhaN05QbnZ3dGtpb01iNk4zakZ1LzE5OEM0VHFlWEFMeFM5czlCWkFwbEZNdW1aZTJPOTN1RnZsM2dTOFNlcnZFQmpya3ljdW1Bcm5IRXVnZ2M1QkZKcnZVTkZpc0hYQjk0OFVBS2NwOXljMWZLSmNKRzRCQ2h2elVCQ1RKUVVmTThYOW9EcHdhN3ZMK01sbzlsemllMThxNXpkVEZqdFEySXdPbVdQTkFWZGpIdktNRXpHV3NKdnVSQ2JoRnZpSTUrV3BpWGJDbjU3MmpRYXBtYlFENEtVWmdwUVZ6ekh3S1RaR1ZqUFhwd2ErcFZaS0NtVjYybUo2bjNqbmhlck82dU55YW1tM2NCc0EwcHBvQ2dwU2xidUJlYitwd3JEc283NWJFMy8wMzhaNi9wNWlya25pL3k1K1JqN1RHY3A0NE5DRVlOKy9qUG4yQk80T0hJcjZHUGtNQUVkMTJWMU1JYUhVeVI5bFBzOHJTVGVXVlY3aGVXRG95MTZtaDZVVkxkRE81c2xJN3duNk8rbnYwKzdqZ3BpdVhqcGNDa285RWtyQXNMalJvc2VDcVY2R3d6SXF5NHBmakR1OUU2NjZ6eHZtZ3VpMDBHZTlUSEROeU9LUjE2ZVordkgvaWFWa0ZHemZPYzdvTzFmeS9DRXcvamlPQ3VQRGUyTG05dFR0MlFLUzJFdDBlSmRtbE5lVVh5TGdDMytPM2xKdkN1WE0ydS96cVhNR25ZS3RxTitnVmlVK1doYWtlbXBXSEFRc0REWWlSVTc4c1daUjNYNHNVZHpnRldxMzdXYkl6V3Zmc01TYlRJcitwTG5NODg2UmdFVDZOdXJNUXhlK1ZvK1o5bnZIN3MvNklRa2pUdEQxRGZMMnlpS0xnNXZvY2NFY216Q2F3ZDQza3FtSG8zSHQ3OXEvM2RGWWFWODRRUUxjd2x1ZkxXaTRGS3FXNEgvV0FJek9MVmtIVVR0LzNUK2t1cUd5eEdVOU1GSTVWMHdnMEhqVm9rbkpFL2xtZldCNWhIeksrWVRPRXovWkduNXN6RnNKRFQ1eWs2U1FYTEg4b1AwbHlnR0NMNW50dFIzUC9IMnpMUU9JUVJ0VG51UG4xeUw1UWgycE0wWVZkZW50eHVnS3R4dHhzSkUrcnlEU2I4U05tNjh3YkpOTC9pT2dQN0hKVHBXRWkxRTBvcVRaQVJYUjZUSzFGMUg2WFNJOHl4Zk9rTTAwZXROTkxDSnpROG5mT3NIdW50ZzBOVFJXdVBSLy9zcHQ0NVVqUnlXK3BIcGRIRU13aXdMdDJuU1dtSEdVQXZoQSs0bEtCd1RHUHVHb0ZuRFRjSmh3clliMS92aENIaGVFWjVwRjNYZzZKQ01qWEIyRXNhUTlIbEt4RU1ySGZVYW1RaVpFSkVnMDZHUXNqTWxzNlhPSEJ3aUpiY3dMdVRNTlc0cGRMV2tqZDJYMzhkVW8xM201MnlaeE5EUEFTeU1EckVQRzlhaEttZEVPRVAwR1lGbVlrbW8xV1pBbSsvN1lVbit1TDBiUGVzbnA4c0xZVU1qVU5FSTlnYkJYTnF5K24yL2JzQjhIZHQxaXM1cjhPV3djSTVsMkhucVAvTEtBSmF0OFF0aHI3THNJWDRwaVdoTXYzb08wMGJXZzdVamppcFZvQ1RCaXpzeHM1VnpsNHpaN0RpL05HMWZIQ3AyUnFFRjBma2R1NHRYMTNVdUQ5N3NLWEVKYkRwQmpVRlR4N013c1pDTVlEcGh0Mk0yYUFzTnZkTTNtcHZrNmtKRlhtKzhIUEV6ZVZYblplTnJkM1ZQT29BTFlLczNBU2RUa2xxamM4K1ZKaXE0eEUxNUFoVTZ2VW5pR3dndktKUCtBSHp3OHN3WUhDaENLa0RadlZKR05RQ3R4dFBUaHlPTjRSU0JIN0hWbjdHL3RZUUUvelk5RU9ReVl2S2xhY0xzRktqelJabnBONGg5WWo3elFUTEQyT2N3MjZDbEpyMU9ROGFhR2c1MTkzUlJ5NWJlYkh1aEFjV0FHcWZaZUd0bVpISHV6STROa0NRTzRRczFRbWZ6UE5oR2RLYUY3U3htQjZTZWZXblQxZW9YU3RoeTRkUDFRVnlhbk1VN0tpWjg3ZHUwczZJVmk1TWhSQlYzM3Z2TURlOFlwZUd1YktGTWhqYTZacHNWTUFYZUdUeFJmLzBrTXdDVWFXR09RY2lSL3dVcXZJTmFyZTRSUmI1aml6THZTK2NvZFBWVTN5NURya1IvMUs1bkhyS1dkYVBtVzJQaHR4NTVJS1Q4R2ZueE0yVlp5V21Od2VwS2hvbzVzYnp0Z2hETW0zcEZSRWQxVWxDdnNhQ0x3RkgvWFp3dGF5R2hIa3JMdEhCSUdjUHVBU3VOMEdPM1B5Y2ZlRnNTemlnSFlCdGxrcnRLRndrYjhQTlkzYnltK3V0eFc1WEN4NHhXSkh2UjBmbGgvaDNZdEVoSVhnZ0ZuemhVR1RGeGROd2pUMVV3WWtZQm9BTTgzZFVQL2JkSDlucEt3bktVejFzcjA5STl5cGptL25NaTZuWlFPNlcrYUZmcjkyT1NTQTQ4MlVnbi9uQ24xdnBrUHdGYmVYOWlpdmlTZFlMU01kVm1HQnZmekczc1AvMmE2TDRpb1JTR1J5K2FTQmpvQitYTlhKbGlFdUhHQkxqd2NleVNNZFZ2Y0pHc2V5SUhsYkYxeGxpNnpGT0phZHJ2bklVVkgrcE9uZ3Nnejh5ajgwamRvZkxob24rVFNBSDd6QUhLY1NMdTZoNnp4YW40SDhoQU43VTMrMXJDeDFQQTJCMGM2dlNZZndRZEcwWmNYelppbmhGaUE2N0NKaVZBbzhlQ01hNDNHeUJvRkU5SGxJeElZK1BPVkdCbFArUHY4WnUybDZJWnVBbUVjTmVScUFQbzlXZGMvYkRvcytKZWpEeEpZR2U2QmtkN1dqTXd1L1dtVGZRUVBtU3F0TitIS3RJRGRSUGNSSW1TL09ONE96eVpzZFF1b1pSb1NSWWFMUSttdlBMVDdaQTFUalo2eXkyNDVLTTgvNzBKT25YbngwSmY3UThPbmw2Vmlrd3gyZmh4MmtqeFBvb3VHQmdEVjhFdFAyUno5WEhMRVVFdzFRaS9scEhGbjZVQ1hIL2tCS0liVmloZ3VqZHNlUnVqbFFLdnhkMFdxV2I0R0gxVDVQQ0RyeSthWHBrdW9FbzBjZWlKY3Jyc0d5V0VrK2p6VFZlZFpsZnNhY1NUNjNMV1R5azdDTnJxbFhla2VHQ2JReFBudktLRVZLL3BnZzJxeXMvOGgyRitiL2J4Z1ZhT2pjOThjVmFFT3pPQzkrcllNM1luR2FDYlpQNHBxZzNOaDJUVllzMG1CZ09kOEJycWtpd0ZMYW52amtXOXdYdU1IZDFhU1FydGJ1WitKNDYxWE5ZQU01RHB3UlM1Zy9nMHVGdmFnTlBQbXkyM21henExMFlscTQxVHlmbWNEVlRUZmp5K1RtcWxrVlBZZlQzMGQ4WE81RHkvKzhxRjJqa3pYMVIvdXVSbUkyT2xuVDBYZkxzeFI5bjUyZmQ4d29yMyt1WXFzRW95MXJnUS8zaWlQMUcwTkxiKzVVdTlURFlyNHNwUVpMV1dSZ1k2N2pXWnZWNkJXdXFLVjRXbWxsdkJrbGFLbyt5NFdRRXFGRm1SeUgrQ3A0RzErV1A4dnBMUTRUd2dqampwMEtDVGV4ZnE5eENYVFBCRVgrbnZSMGw1MFlydG56b0tBR3AyR1VMUER6OTNEZUN4Mng1SFpyYTE1Y0NtU3ozbXpRaGdQdWJPTW9RUlpNZncrUUZhZVRxd3l6MythMTJFditpYXVkbFQ5MWlXS1dLYkNadW9jc2IxU1R6MTRyOVNBOFV4dWFrWTFsZFprWVNPdGhtZmttMkFLU0Zocjg2TVlTL2NSb0JsWFBLeFlsYktZeFp4MVBEamNIVFdvbGhXc3MvL1NjempwRmhIVWNZRUxwaEJxWGMxQ3BkYnZvMWpFNDBxbEVWM245dG15OG1Bd3hDQndDZE1HTGNJQUZRSEJOS0ZwREhvNTNpL2d0RTRqalphRVJ1RjF0b0c4eEQzQWh5QXlUUmV5cXFCYk1QZEZlZmwvVG9CdHRqempkSjRVTmZqTUQ5aUNoZ2Q1cHk1U0N3YkNXQ0V3R3I4TVY5MGJncW14eUVQSTVoVkg4cUxsejJSRkFmL0wwWDFtUXFWSUNjZ2liTlZpNzVRd3p5aWlzdTEwbTdhLy9jNFFaV3NDYnVjeXFHWDhONWhiRHBwN1VmTnFPZlhmeC9QeWsrL0NFM2U4NlNMYlF5My8zVzVRZnRuaWorQ1VaTVpreVU5c1Z3Y1JPYmFrT1lleXcwT3hWYXRyRktZajNRd1BXdGV2OGdUdG1oSnU0YWZwejVFOXc2OVVnK2JFT2ErVTVXeWN3d2gxZ2p6dXI5UGg4SERQZHVCaEl6UWRsSjVsUWVSamJ3amVJaHhORndnRjMwb2I3bWhlZ3FpL0k2UmZoOU9zTHgzRVRRT0REcmZRTFFCM1JGbWhsN05QM29wajcrMGhxS0JHdzE5a0RJbzEzTWt5YUFWV0pTTjExZ1I5dStZV2EwbWw2Y1RTUHFEdlNWVndLYUpDWnZVZThuMnJWTkd0Z3h4cWpqa3hKOUltSTVhdklIb1ZnalNHaVBsYytVQks3QkJvQmg0YnZMVTE5VWZZNmUyNzNNem52MThxaTJnOWlST3lDYUJGSWdCV2pVK2pxUTBEdkZFWHFDUzlER3BUTHJkZEhIQy9qSlBoZTl5aFJwWHB0c2t4ZFBycXJqQTFPWGRyM1FvTHIrV0g1NU1kTis3bFdVM0ZCUHVzQ2ZHNVUrTmd4OTdkMHpVVFlpRHNXL1NuckhaanQ2V0lxYXJ5SThYNXQ5WGxuVmNWS2xTaTV2WS9YYkNrZmtva2VmdUY5dkZGT09ld2x2ckR6MC9NMUlMMmJ6aTlBRFJPd1FuaTJOYkdGSDh0dmpYeitIUUtxcC9adWdiNVkvcjNJSGR5N0diSlI0SDJMekwyY1FtQ0pQSCtiTGg0c1dkZm9GUDJRNTloQmxMa2pkVXc3THRzTkhrNm1zZFRTZzZibkNxeThBUWpYYk1OeEhYbldBYW5PQTdnVXJJT0RZMmQ2UXMvekVQUjRnaFdVMFJEekVMWmd2bElOVk9pRkg4QXBPZElxZHdkMzEzYkFoU0h1bGVUUDhZWDVaK0R6MkptUXBPSmJEdStoVkRFbjY4eWxpTXVJMmEwWUZvbUxSejJzVXZCWmFPcDdIaWwvRXBmTERCS2ltdXZEWVdaNXRWTWVOcjliU2hIcHNOV3NoN09zVnlZUjBJNERCM2tRTnRUM3FxTmJGUjRtVjNZb2duWE5JcCsxbnpTbWxNV2dTRmFqa2lmUkpEMmZmdU00M3FaVlluWUg0MDBaekl3aWVwRHdqZE5xYlNDbndwNW8xZHRXYU1Ra3gzYThYbS9pL0NTTlJiNlRnZTVqRXZyT3ZrOHBtQnZBTW1LWEVDOWNmUC9XbU91bTVIdWsvOFZrdzM3Y0N4cmhXeFJwdHNqQ2xDd002YzBLa00vK0dIOG9jSkV1MjFBTnNjU3dLSGRFRXBneVVhck5hZnhvOEVpSE1sQ3pVUnowMlZNYnZrd0Z3L00vWEM3WUtKem1sZ2Q5RTRkblJEL3ROZWlMNTk3bVdVVnIxbXpHVzVxNVZBS0lUUVd4S1NQWWlicmRQa2owSXBYMk9OYzR0YmEyQUFldS9ONXBmdWJrSEhBY3V6VjhpbGo5NlpQNHZoTXYrUk8yb0UxamdRZXZZWXY5K01oWlhiRjU2WWlTTG9zSTVNSEIycWtyVFkwRjVJeFdreVZ3eTBONXdrRDFnZlJ5cUkxY002Vm1ZalRPZGQwZ09LNTZMbWVlenhDQmlYYlJhZUNPUmt3MGpXSy9aZ2hjb1JidzNrTEZHaTFoUnc4UmtqYTk5ZDM5cFA1dXVpSGlUOHIxaVlHTS8zNXQyaUtxa0tNc2tMQnEwMmJnTnF2MXhHNlNybGQ0ZTc2WC9YbjQ0b3BRQkFpU0JNblRrTlQ1STV4azBoYjExOFhrMHV1QzcrdnkvVm0veTVISDRzTzRiTmUrSGNtQ1AzZk1JelY4dkI5WWw0M2ZsTHBiZFU5OHhmRlJ2ZEhIdG1DNlE0MHBsemlTSUNOSVJyOTltd2JsZ3REUEpMUEYyZk5wRU9vZlp0aUVBak5vVE44Sm11dkZaZko5R0FicStFd3A0N3o0NzQ4eWN4Tno1SUUyQWhubUVWN1ZMZllXY09MU0RzVTZXZFFqb0EwZ0hHcmFJWlBSR1R4dFo3QSsxU1p2Z0tyMHc0Y0ZmOTRzT3hvdm1McktZb2lMeGhJcDZZNldIS01xdG5HR290VjFOcm1kZkZlRWpRZERodWk0bHg4N0g5WCtxM1hwbnBFRU1vckg2NnV0RGZSSkg3dGMwQmhwM0gxc3ErcDRsOWp4bUdWTmhsNmlvTjdrc3BDeEJEVUluSXFiQ2ZYcVZ5Q3V0TDFaOGNlTDg4TEFBV1Yyd09ySEpHUFMxUnVsb2FPaXZMbkc4RjRjRUptcm5renhGZWwxQVd0bnNLSTFwZmhhczlSWXFYbXhhTlV3QzFFYkRnQnZ0MzR1eFFIYkpObkdnZUdDMWtkK0NYVGNMQ0FWS2NaWWR3YUZ5eHovS0tpTXo4VTdGcG4rRE1MT1ptYzZKYmtMblFodVNwTzkxazlMNXBIYkpTUnR3cWtNcngxK0NDemlscDFjejZRRjFpUE9ZNU82N1ptdEs3Z0xyTkw3QkEyMkg4UkJDRWdYSjdtTlJRQ0lvTXpTWkRaaHhDclAyaE42elpuMldZendsZE9PenY1UjRpSzNQUlMzdGFHak9zbjRYSWJ3aHg3UnVpcXoxUy9IVTRsRkFZVzE1bW1mQWRteDJIdlhRa3hZNzhJczNNelFkWG5TQ01iTkxGT2N1QVpLZVoyN3VnaGNBSEZwSVQ1c2R1TWtMVTBMMUoxSExELzBUaUFIYlZSNkp5Q3NoS0c2aDQvbEhYNlVzS2U0QjRVT280NDlXZEhRU2Vidy9nWW5MUnJtL3d0QnJrY0w2cjd5S3p5SjdoanMzZSthVkI0bWNoamNVKzJZRFY1MFlKQ3J2Z0hnRUlBK2NJekd0UmFiL3h0bm4wYnY5YWx6MzROR0RpWm8zQzcyU1h5cWN6VXBaejRVdXc0eUlDVHkvdC9wcW11LzZkbEtJR2xuSUNqemk5RWw4WVZqMCtNdS9jb0VGNmlSQkE3TUwzKzBiYlh2VGRpWDZkdUN2clFiWUN5SVhWOTlEa2ZzUVZEamVlS0svWlovQ0ZoQkU4YmF4bEJtQysybHpFbnA4Tk80TUR1Um02THE3TWdGVklWUTdZMy96ZVc4Y1FPOWdGWTErc3BjSXRyZ0VpTnJJYm5PU2tYaDNKcUxITmtwdXBFUGU1NDVOTWlEUUlKZloxQzFLaHFxeFI5TEY1dXpQQkRQUEM4QkVkaktFVngvVUlaRVpWSnN1aW9jbDVjZ0JiNWFoRXkzWEVWQU1mR09wZ3lvTC9jcSsyMU5McVk3TzVheFlxb0t5SU9ZL3AvSGwwSVpJTTMyUEI0bitkbWllVW93Y2xicE9VNExkUlVvc2dQUlNtZHg3bjlHU09oOHNrM00yaFh4RitsVVBpc0xNSml2MW90ZUpySzI4NDZGVUk0MzRaVTRLKzJjdG1NcHJIeTQ2UjZLTFlFMTlpYjliUVVrWE9tK25jMVdKRFBXVHZHQjQrdHI5K05UVGFteWRHVXhkQ2lYV2ExWDFHZXcxSUNUQWdvdWZpQzVocWhXOUdOaXp2Z0E5NlRtZFRzQ29ZK0VXclV5bmFHYzZON2NKTnltbWZLVlVMdFdaS3FoTmx1NmxLMnRPell4ek1mT0FYNmVVUVhyWTFFd1AvM2pHNHRuN0czK3hlL1puV010TmFFUHhKTzNmVHVwWlNDemlxZHJEUkYxbjRnT1lFRkRRdXdWLzExZXgvTS9NR0k5WTNPUkQrdllDTXRMUkwwS3ZheXJ6STJMd2hCdGhUSGFZQUlLRFR1ZTljTEIzL3R4NjJhcHdMRWZ0d1h2QkJlTXFuc21nVjVnVkx4SDIvNyszeGRuZUFKLzhnTVVjMS9qUW9CeVRHTGpZbjBFWHk5elREN2lRdkwzdmNBNldCcTBmMGt6NitMN0VQaDZpNFBPMWU1bEMwZHdjRDZJUG55d1V3V2x1R3g4ZGtUdDRxVktxSzdjRlhoNGMvaEhsSDljQWNDV3JmTnZtRWRiaGh4d3Uzek53VHdOMUVRdXJwTDhjbW1NQkRNRklGdFlNT1cycG5EeUZwZEtDZGRTbTE1M3ZmMmVRYmpJN1pQdnc1RFpoY25aNjRsd1U3MHM2NGdFdGczMERrbzRKSkZvanYzWXBBY1hKL1pkV3BPYzBTck9tSjVkWTdCZ0FReDg2NXVUQnJGK1puamh1WDlNMUxGZUg2cW4rTGlEOUpYa1pvenMzUzBEZ1hZK241bVBiMXd4WjFwOXMwekFFQ3J3WHFaUGFFQjVtaHBuZHdvcjFIQzJyanUxVmd1ODlRZlgyRTV1dXZZV3ZzRmEyam45NDNQaXVrd1MwbnJIWHdwUHF6Wm8rK2FYRXlWeWt6Zzl1eWF4ZWpUbzNlUXQ5VGlsRHZiYVl6MHhCdkNYU3BoZFh1SHVJaDdSaUNQKzYwbmFEN3VzWWFBSC85c3dFb01vQzVqNkY2WUZTZnJqYUpvMFpRMXVpcmtnNlNSaWl1SE96SHhtTGNIRkwxcTJ3YytlOHIwUDZrcXZDZ0k4VnYyRjAxbkhvY01pNFlDOXdEQXVDeVArUHhrc2o0TXNJK1NaS3hKOWQya1lDOVJGOGJHd2R2a016a2FtUWxFZTk3dnZHUEYrOHQzMk4wdS9NRjlnVTl6K1ZqcjRrb3gvU1J5RzZmY3dPSjRwMnc4djJkL204a2YvVDBodjBFZk5Najl3YkptY0F0OUdtUk51a005MjRTc2VWaVVtTkhhV2F1ZHhkUWlSK3hLYlZUVXozc3lZMFQ0WDBkbkZrV0wyWXVLaloxcXdZYng3U1dLQ0FEVnNadzRDZGZRbE1JZ1g3L1V1WXd3Vm5HRURPNlgvZGFJR3VHSjF4dE9PRy8zVmlZdzZ6R1p3ZlhXZTEyTk85V2Y1MEVaNEFrVHA3Rm01QW5QeUVBTDFNUHJHRlFVRXZnQkF5eGlVbWRacWFjNjNpeXdOemFqRGxPazZFZUczQmdubW1RaTVOR3AyQks5SHV2ZUdvRCswSW1QRHc1VkVUa0RvVHVaQW41d2FtUXZqR2xSTTVzS3dSUm9ZNmVtWkRYa0dLRmVxd0c0NjV6YlZseERydXdSRDVDaEZiOWFPcGdWTURMU0YwYW5wN2RVZ2VCR08rQy8xQlkvVXlBRWFBNW04VTBVbVorTlhqbElYU0Nhc09EdXRwMjlhQ2VTb0NZYkUvUnJqMzMyVXZMcklUdWRDM3pVblFWbkRIellmaTZZL21LeVRRTklSMjUrbkpCZExzYmZOWjBjK2J6ZzJCVjZtcGJYQkVRL1FYWjRGU0lhQU5lbjJWODhSL1BtUkMwZUJhS1pWMEpENmJnbHFmYW9LVldnbDhvdzllM282b0tRQUpqWWFvcjBleVVNZmcrd1kwYUYveWdleWYzUnpveElYOTVoVHlHbUt1cXh3TVprTGM2YlZWUkQ1S2FKU2NESDU3cDh3RGhGUzVQLzhPMEhicmNLM2pzVG1Ydmcza3ZOalQwejAzUTI4Mk8yZ3lxbTYxWEdqZnFlZ2RWSWNQRzhFZmQ2dWJQUFpqRXdNMy9HbC9nbC9aR1ZLS2lEbjQ3T1lOQnYwbVplUnBMcDY0SkxnUzdzcDE4RTltZDBMWVpKeldQS2NpUWxSR3NiNklHeHNmcVZKODJMMjlFdFVYODVBV3drUjB4WE9BV0FOb2t3cnBQRk1HUUswbXlSZ2ZEaTRJUzgrTEpYc1MyR0NseTJXOEhsSFVNZkcxWlgzY2NrMkwzTERxNml3cmNjek00eWNnTmhoaXpJV1gxZzcvSTZUbkphTTFNTkZMbnNhTFVXWEx0TE5EYVNoeSs3OVFVSFdvSk10TUUvd3dsZW1xQVJrQVNQcGxEeExPR1Z6aEFwWldobHUrOFZMMmgrY1dIcDRlZk9QQkw0YjVzQnBjZ0V0K2U2WGg5Y1Q0akRGTEdNVENxekxwc3BNTi9MTVI1SnFxODBTekZXclNuNkgxK0hwdW9GZ2tlbGJMZTFxaVpHREFBalpPaWdKamdZSWJScmxROVViUVlJakRvazgwZkZrcVYzTmo5MFFZS0pTNnZSUlF5WE5DR2RPNDZ2RS81TU5sbktmdVVLejVacE1FclErM0g0emVWbmRjN2VGQi9NTHRDZkRsZWUvYXQ1c29Sam9Qd09BNjdsdE9hRUY5WWhURU9ML0lGazhHVXpNL2JtSFpVak1lU0R3aXZMVU55Wld1MXRETUdMZDhOL0FSYjNBRVNHZmdWVVhPdHI1Ri9heGRxNjlQN2paSTZYTlJad3hpaGdsc2U1Y0UyRDYrTHpjMGhKK29qRHlYbjhWWXZoZU5pVEFjbjdvRXdGWDl0R045TVFLTmtrLzBnaFYrSnJ6T0JEQk5COG4xNkRDc3RXOVBYdFFtN2FRcGJQMHA5d3I1dEJaWW9KNHVDRWVCemduWDRNcG5vemx5Zk05RTNwN0tuVDEzZzN2b3JlNWE3bko4c1REdnhlNnVFaXYxdEFtQTNiUXAzMlVRSTBpNVorNVRTR3hsTVlmelNBbFZycEVxcmthTXZpNmlWRENBdlI4V1ZUM1EyelNPeURkUDlBaXlNcEx6TVNoNFNmb1pKaDRuTjMyNHpGNklaNnBMWS9oNnVlOVBLRDF6TVJzc3Qrd21UL0JFQjdzZzR4cDdJaWRjT2J2RnVQaXpIaFlUTGFzNVlLcHBaaVI0bU5MdnBoa0J3bkNOeXllRDJidHFoTGgzekVIU2ZRMFpyZ2ZKdlpSZCtWVmsrdk8yZUVyQzdiYm9zTEJ5eUx4Yk5Gcjlrd05ubWFXdHA4bm5Tem5QQ1Vzc0pjbzZFYmlzaUNsUXVqTEttZzc1S2h5TjZIRUk3U21McHRuNHhHUHphZkJmK0pCZTEySXpxaFY0cDBZRmZRVDN2OEJtNytwaGVnemkxUzRGNEx4ZDlDNjBzS3RGUUNDQnk1T3R0NHRZL2dqM0g0dlkwT3Z0dWpCcGtHVzhkMlZwN3FJc0p0eEw4Sk1Ja0tLY1FIVThtdDB5YUVUaG1JZzNrbm5WQU1ic2Z4WHpRNjJhZXRJVDVkR1o5alVOMTF3bmZwL1lxN09RUnVyOUlxbktWYnpyTUpGTDU0YlEwakRtL2pkbVJ5VDljbisyV0htb1dZVGF4R2M2TkpCcWR0aUZJSEVMRThXQ1VQS1Y5K0o1NDBNT09RQ3hYbGJyUHJ4YVk5dXhYQXh1bUl5U1VHamtlbW8xQVRJbGhzOFJvemxsZmtzTmttYmJnaHRvWGJBOVlkZUhwQ1BCOXVyZTlOL1hzSmU3V01hL3VrcjlERzFEUTZEblJKd3J5K2lJb3B4NytDUHp6S1J0L3I0bzl6L1JqeXFyd3R3UXVzdXZMNnJobUg3OWNsRDJrQ2lLaTFOSlRsOHQ5Y0pDTjdvTzV1ZCs0bFlPTW1BYU9oeXBMSVU3NmJkWVpjUmRLT3NSTy9jZTlSeDZGajI3OWRob0Rtd2l2V2d1eHd0enkvT3pRaXNvOHMrRnVVTytCbktWNnkvNTYrbUFmMlY4TGVEVWF5YjN0b3hOQ3M4enJHS1BqeHNpOVVWMkFEZytUMTIyOGZ6RkNhejlReVIwdzRTL3liUlE2SmdnZ2xWdjRGbm4wRWJ2Yzlqck1yWUp3UXZrM2tOR0lmb1NHQWdSdURYMHFTdFlHb0ljUVA2UGN3MGJaWDdUUHd3K1dXSjNqVGU3eE5DMm9nYktoTm1ZNHUxNVljVjhtbnExZHk3TWZGYTFPZmRJYTY0TjZXcmh1MmNhNWRsTjhHVDRYNk84ZUNYYnhPOUF3N0hZTHRISTVka01mbFRxNnJPTFI5ZC9zMVZSZjRScDZqckJUcVR4cTExVHcwdUU5N01hS0ttVHNCNGh1eTAyeVVPK0dJNnpzSTYvLzl5YjB2Njl4a1RkdWw2WTF3Y1hlTFhqbVc0Z0lKNk1teVdYU3lkMk5EZFJqS0tFeWJScHBEeE1td3M1eGpONVBmV2FvemliWG9ia2p4UXUwRExQMk1SdWpvdVdtRjg0MmF2QjZTNjF3b2JlV2RqencwdS9RcVNBUkVuV1J6WGptSVAzUk1DS29jZUpITmlFTHdvZGRWV1BaeVd2ZTExRHBkM0IwNzJCcFl4SHJNK0R0WXJ0Q1E2UDBIMGlYNEt2TzZlSkMzM0g1SUtHSjB2OUszZ1JlbGxubzMxc0ZhdUpLNUNtdkpFc1NrNFdlcFhoeGVXaVhGTmxFZFNEWWMxQTRyZkZtay9idkRBTDlNTXhteEI5dUNkaEg3S1pVdmJ3M3IwZTFWZ1ZGaktsWnQ4TlFIOVAxRGgwdHU5OXdQSEhiUi9XVitDZDVaM2t5YlI5bE1LUGxjcXRiZ1QrV3RFQVdrcnNEVXRLRHR2dStFWmFNR2xiaEF4cGQvREphMW1hdk11RHdYMy95Yjh3OWhRb2NFQmsvdDIzV0FTMjhJd2ZnVUswMGJmNFB1eFVDVTJQeFd6MjRibzQzL0Rua21WM2llRUZkNUlLcXUza1RSMEpGMENEa0ZSVFRhMTVDdjB4aENrMmNBWDRkckJ4Nzk3ZEMxdUo3dzlUZjRTTHBGc1Bqc2VHUUtZTmNQWlIxWTJFeGM4THpSVGdQK2t3UldZRmp5ZThzeUZ1cnJTMWw0bmRnVTR0Z25RTHgvMmhGZGd1Y2dSeU1oTVJmMEtsM1FhK3pTUXlsK3Fjb0NCSXJyVGYwbUhhamRJWXJLcWZVT1BOUzlHU1RYNXp1aWN3OU1wRDMyZUw1ZmZwUkd1VDFGd1VyZ3dIc3lLY1RtWGFUNGRnU1FtUnI2OGJ5cnZuVnY3OWsrRmlZNFJ6YjhOUG9BNUZtaVFtWlpWdm9VcXltd2dxbjJLWkcvMmFYNUJwaDBZWnVMdEhva2JwdnA2RkpVckZVdWpBTEFQSWU3MlVPVUdPcmtwUnY4c1pLTktENHFwTFF0UG1sVXQzbHpDWGFmVkRYcm9oN29XT1h6ZW11QmZtUENoZ2NlUUdiZ0NRbFo4SmdEZzB6UlF5dk85ZXc4Q3RySjlMT2R2UllBYm95N21mQkFWU3JRLzRkSUZYVktIYkM5SWxQT2czTUF1SUJFeGdZRVVneVNydWwyQlFlMGNWZXhoemgrZitQQWw0WkFhamZTRWF4cmwvOExVczVCSFJyRkQ3RXRadWh3MlV2Wkd3TEhYS0ppazVydnRyOEg1MHZSUTZYZ2RZQ2NxK09RRUlIWityeFIzbi9QWEV0Y29rOXdxa2dhS1JBTThBSCtpaysrRGRoS3Y1YWs5NWU1V2JLVmh4VnovMm9TVjVRdzdvYzBUTnUrVTFwa0g3blFGOWEvK0lwaktqa2ZwU0pkRC83WXlTNmRYNnRxRExaN1oxSm9WRjZoaGpyT01EcUFpT1dsU1VWRVNydUpzU2tQTVlHWFkrUTg0V0o1bU5QRUpweGtGaCtIeXVKdVY0OXZNRVVQdHlFMnpvaWtyMnlrbEEvWDBVcmo2RHB2azhIbzBmMVQ5bWpwZXo0SDZZR3dqQTNMUkJQcVRVU3RRUmVjR1p0b1AxZ2R6Umt1aUJvSW1Uem5aUDFGNDNid09DcDJOMkp0Z1FZaVJzZ0RYdS9JZ0pua3lKRlFRUTArLzN6UWdmY2pOUGRIQmd2TE85RDhpUDV0ZjBLeTBMdXVyWThUcStmVnRRdStydGk1VlAyOURpMEFxZ3VrZndWUmxib2daUHdwNUJlTENsZ2JROTVnVnc3YVYxdXB1SjI5QXRPUlBTTzdpeVpNNDBjZEpvbVQ0T3RNTWhjbHNHSHZxVGl6Z2tQU1hLbmJXcU9rTmVOdW5BR21lSnlYaGJRbjNHaGx6S0t2bEFVRmhTUG5sZXVjSjUvM2d5b3BEYkk1akNmOFAvaVlDRlgvWVJkbXU4d05Sc2RYazFhTThZSHRCREpiNXRpaGNMa2djNzg5OHhkYnM4allEMUFIc3pQWW9Mckp6V3BsWGx1RVVyREZ4czVaZTY4dVRZTXE1cVA0VXhOb1F0Q25nRzdpZXJFSm9OYzhBL3MvQ05GM3hscWVPT3JkUS9YMTZJZ2ZabnprbTZVUlFwMjZ6ZjR5TXBxeFJJeThUeEdmTlJSTVE0Zm84L1J0R3dqRFhBV296MzR4dGJVQTdTTkYzYUpwWEdOSW9waFZPSTF1MllJdEZCazk2c1BCaC9vUGtpY1gxcW1weTJrc1k4U0FaSks2MUhEVWpmdFg3UHBpN3ZlL2xSSUJSMnlFTzFpNG5NaWhCVVZiQ1dXeUVVSU8xY0s3L2NTcFBKRENGVWFYODFnajh1MGJPNmNXYk9JOU9lMkRGUmNzeEdwcDhjcVJkOGZ1TDJIT3g0OCtsaTVTWldLaFMvdTF1TEU5TGF3U1owZm82aFU0YkU3MGxLeldwdUhpV0JWMVdVcmpCRE10MlprVHg5TFEyaW5sSTZleEFIN3hvWGFvYkoyeU9tZjc3eC9iQnlBUXZVKzhuV2cvOUFFNG1aT2Myb0xsWFlmelRFMnBmZ3FvV2tQZjVrakQvQzNDVU96N29Zc0FUK0VkODBYSG9lakc5NGRoelN1MFVQZUhhTUZOT0M1c1laLzdDVGpzczJUdXdBeGdjcVdpZ0tJRTlVVHAyOVNBWjFhdDR6Y3RDVGFNMVl6MVE1dElLTkdVajZ2UHIyOXc3SEVMbUYzOXRuYjY2OXZXblI2Y0FVTE0vSFU2Vk5NcXRWbWNjeWNTQTd3dmNCeHFULzB5MGhneDBKRlRFL0lPUUNqQWNTNUhONFNCWjdRUHY5RHIyR1FES0VVdUprTGRGVDFQN2tva3ppcWR2YWx0ZytLRkpZVnBhdFFMRUtPN0dEQVRFWUdpSFUvREp0RTBndUJTV2ptK3RXeWtqS3NxMHl2dVBGQVFlc0tBaE8rUlhpU0t5YnhoVEZGU0t6S0lQaGIyRmJWc1phK1B5clVJRzc3WTF0TFhqQWM4MmdyL3hwT1F5WEJzWkdrQjMwMExNbG1nZ3BId2ZBdFRScWJib2M3WU9LamhlWU5SQlJnZTRyb3Z0aXkrQU9JMklrQThGRkEyZ0JGTlp5Z3orYTRRWitPMDFVemVYMmQ0NGh0cVpUVjZLdTBORFRHTm1jMmRTemhmMWFhRGVzQUV6UDU1TnNrWm5tdGIrTzVMYmxlWlEyTU9wdDVQbmRaK0hMeGtlV3VHZzBGcTNmWE5YVHVMMjVwTlYxbUNlcWhqTVROays4UnAxcC80QmJVNWd1dm5udXlxYzBBY2lmRDZ0SzBMVFI2NzhJb0ovdUc4UDZjVDdjSDhzMXlvNncwb3YxSzlTZEREQkd1Uy96NUZjZkZsNDFmSEprdHRhWGp1bUZJOTkyQU5zajVNR21KaitsSlJhV0Q2VXdGeVk1Q3l1UGRvY3l0bFVNa2tlR1VvSHc4dVJ0UHNOeFhEeUloOXVla0ZzdDcvZi90a3Z1NXg0bHFiUXlHbWViWkZlQS9rc0JHL0s4bU50a2F3dC84YUF1OXUvdkM3eU5ydGMyYlhKTWlyL0REbFdKaE92aThCMVQ4TTVjT3JmS3A4MFRJNEJFYVgzakVNUlNHaVlpL1Y1V1I5S1VZSFJJakJwZHUrZXQwZFR0VW1ZYUdKak81Uko4R3pNRmxwNnZPcXZQaWw5eWVhMUJwSGRWVFJFZDh0UWpIZnA2SFpOUmFUbXlDSm5hb1ZlVDJSdUhJWU02blNFN3hrMmNiNHBldDV1MmQ2M1dWSE9rS0F0cWlMeDlpZ0R3R2dUWmkxdnZCVWtJb2VwWmNxc0RuMWpoaFFud0dZVnlRbUdGajBDTG1YemJFMEpOQ0ZIUkdQeXdHcWZ3bDlycjhpMGVOSjNHU0VkaWJDU0pmSlVzdDV2QkZnY1V5K0c2TWYwaDN5cDJuZEdRNUZXRDNORDg1Mk9xTlhvdk1DdFQwZmNBYnphNnU5UGRGMG1Ebml2RnBWZzZSTllDWkpnd2JuaVdrTWcrWDdKKzYwTlNpZ0FlWGRMd1FxdzUrZ3RtdlgzVXFZRzVDcWY5Q25tZmRoU2VJenZvYWFYL3IxV2E4eEUvcTk0SU5Xb2tobHYyb2RZd0Vqc01ZR255V3l1MmU2VmtHQjlidHdPeVRGOWxmajB4ZHozQllIOE1iejBiNzA4djAyelpjaWtnaVladE5rWkhhZUJsVjI2ZE5oVjgycDNQTkk3elB1aW1jaUpiSFhuaWMvMHhBS0lXV3kwNUZSS2JCczVNRS9tb0VQTFRKRzdhaTNYZ252QjFjenF1akZGVWFpQ01qR2M5TEFRS05nMUs0TytsdFhqRnc2Y2QwL3QvLzF0RUViR0laMUw0Y3NlTy9KR3Bjcis5bFdmSktaYmxWMFc0YmNXM3FqSUFMV2RuVEQzTkU0Tk9GSlRGdVN1ck01ZlFSVnhQYnpUSlM5ZnVzYXBZcm5HUkh6d1EyV0hha1FSVXZxQi9HMXFIcVlMbE9uSHBjeGxnN2dqZGZTeGJZMVM3azdsb20vZHBmR0NzblRyeFVjVHpnRXVtenNNQ3JkV3dOMUEybnJpNzFGNlp1OE1mMG1KNW44S2NzUmwxQTFadVRPVGRDb2NmRGw5V3IxT2h3NjNlcFh6T21XbWpQRXdVUmRXNSt0UG0vaTNaMDNJOUVMcFVqMWx2dTJ6eTlWbmp3QnI3b0xTdWlzMk0vZEt2M0ZHSTJZWndNbHNtM0Vpa1JHUlRqREZVZ1BRT3dBNHZVeWlGbXZUMEhXOEo3UHV3M29VRVhvVGJFaHo1bmJ1UGN5dzAvWkxhSWhOR25XQ1k3NkxuUUVrV1IwSStjTmZmQVZ2L01JdVFKWjZmcEc5UlhzNFFydEFmbzFndlZZWnVDZlB4eGR5aURsaW9yYlpQUmVyak5aSlJrNUJNUFdKQzFENUNTMnl5U0NWcjhaQmt3RmlRM2VzVVg0Rnk3ODNaVXk3VHV2YXFOUUxROTlKYll5TTl2N0NhUGVoTDBqSUVTZUt0S3BsUDFoNjRsQlBvenoxaVJJRlp5ZStIU1pPcTRpR3JoUnJIL0UzeDFwZTh5Q0NGNVBOeGVkQS9nQXhQaXBSK1YxZE9IcEZlME9Rdm9VMzVUbDMwWUFtQmxUQ1dteDFyQXlkSzdqQzhoNXFkZWs3OERPekFGRmZxWmtpWkJvNFZ5eVg1NjhoeHcvdmRzb0tSNzM1NFl0V3grSDBWQWlqeGtpQWIrVi81c1NLS1gvYlA1N2RvL2dFRkNkVTlMSDdlbUhDTmRQNkR2K0Fyb0hGSmIwZUpwdlVwVUpsaEplV0RHZGJJSEdFZ2hsVzVDakVBLzNWSTRBRE1aYzdUZi8zOWxKTy9Ldmx5ZmU0Wm80VlpwS1JUMVhjK2o4UjV1OWdCc3FRRmlJeXNic2VySnZQL1VBQ3Z6Y01iOFV5OFJZWnltL1Z3a0c5NzdBSnVydkh6ZElaK1p2S3lVS2tqNVlkOEdyZzFYM295S1lBQjRKYWJjbVVqQUhBOFlHZFV2NEp5Y21vYjhkeFdJbnRobk85VGs2cXFmZ0h5bHJQUUlpTkJMZU4wWkk4T1ZvNkRlRDVDV3FtVnNMK2hKT0k4dGlZUXJCSXp5R2hTSSs1NUt2bllyU01MNlVUalZ4R3dLQWd6WmVUeVVhNVhRci93eS82RHRTNi9paXRsQSthQVVPdnhDUHN5OFRRQ1owVWJXYjZ6QzdwN0VXc1dMWEhjeGpIa3Vxd24xMjkzR085TlFXQVlDbitQcElZbWhXUUZrTFQ3VVRKMnkyeFVEUUpveFhJaU5mUHJ6b1kwQi9rcU4rSzhHWTdJaWVVVktscHNuU0xBMzRmSXU3NStwU0czQTdsYkN4RkMxQjJ4eWNFUEhTNFFIUXMyY3NyQndvN0dmNGNmMThVWGdUanlwRTFGVjdZQ1RackVwV29iaXVKZ3lPNlhjK2duN0FqVjlFeDhGY09vSTRTUitGbEx6bkxoQTFMcUVYcysvOTZXeVlLRG5FR0pPTCt3dVBTSHJMcXZnN2VqOGhKWGkwSGtIZEx3TDNNRnJLS0IyOGxKekRGbXBXMEtwUGVrNER0WGJtYm9Jc0QwZ0RxeUJKTko1Z0FKZzhQLzYzbTlSTFZPMjBmYWUwVHdKR21mREZBakgzWlN1Znk0ZjFKSVFMTGljTDhKR2VxaVc1UStQYzQ0T3RKeWZxZEYrWDRUY3VRNm1IWDVqMEdCQ1FCQVd1ZlBVODFxQ0R3TVF1ZzJrZ1RNcEUrdW4vUStZdXU0SGxrcmdJaHUwamtPcXpYMFdVMzVBajBrV20vVTg3YkJxMkRnUXZyUzFDTjM1TmxDS0hEdkNwVWh6Z2VVcTlrbjhTaE16TjFKbjNSYzJkNXpXVVZCdzlRNGd4THd6emk0R3R1clhwSFA1KzNOeEdjL1Qyb3lHb3EwMWc4Sm4rbGI1NDVrd1UwcDc0N1NVYThJaWxTdnZWdi9jaHFYbDhOdk5rU0VKY1E3SGdUczN6R2RySE9rZmFjVnl3dlBnQ3dtcDhDU013MXZtL2pKWTV3VkhRKzZpNU10VWUzL1RlZ1dwbzAwVlU5U3diSGRWS25XSFNPd0s2OEJMcEViYkFiMXNTSGk2ZVlUVUVNVlhoVFVrNVgyaTNaM0J3L3R3YXhhQmhFQUdoc1J2WGlYMjFQUTVXSzVYS3BBWHRiSWk1ZlVLUGtsSUlXY25KVmJUcXhMcXNHeGF2WW1BaHZoZzZsbFhTSVRvbFZLMlZZT2J6S2h1RTNJRy9pL3E2MVh1dytaMDNJVStJdTVrc21Hc0dIdGVRT0VYdTVQTExjeEJEYjRFTDV2NktGbnZubWhzaWhCYWF6UThVYTVIMVlxenlsckFaamdMZ25GUEVxUWRoWHkwbTNSQk5iNnNNTEQzdHpJN2xQMUVJMWk1S3lqZ2dsWEpSbmw0YU1FcWRxU2FkNzdPcWVGeEowVDZobWJPclIwNHo1RTdDQlZNdjd6bUZqNzlqY2ROMlRLcUowOE9pRGpJT282dTM1ZHcySUl6UkUvZDVMK1RvU3U2N0sydWhSTEJrR0ppVWJCM1J4VHkzc1FOcXpoT0hPTDZqbUIzcDBVbThMb1c5NE55WEFDQ1NoU0p2cjZCVzBNb0dDYVBaRDlJYi9kVW1HdVRvNFcwODMyR0lSV1c5bVFxNndhTFJZakZ3Q2RGS3pmRkJGODgvMDByOFJlTWdHaU5LM2NiUGtvZ01kQk5HUzBlWlcrL29lZEl6b0lIVVRWc0h5MytDRGZYMFJaYjJuMkcySk5BWm1OSEc0ckd0NmRZSTdoTElqeXBPQTNFOWttL2J4OFN2UzdSMGQ2RWZQOFBITHZSenVUbW9WbG5GTklhOGQreGtqSGpXdi8yZ1ZJUE1RWFgxUDJGZVRSMjRqakZlcHdoNDBjTXBySmtEWnJsUWFWRCtMWmloMytGZ0paQ0I0eHVYSUN3ZVVKQ0s1T1RVTWJJMzNsQjJCRm1UeEFNanZZL3dDa21XOVU0bHE2b1VhcWlOOGZZN3ZCK3BuZWJITWdjditxREIxUUtuUjByZ0xmS2toQkFQWlpZYjJ6eUVIeTJFSVd3TFprUUNhZ0cxaG1adHFZU0Y5MUJ0bmNZeHo0YytQNWxwcWZtelQ1bjRybFhjT2RlUEdTdlFjVnFsdmZVNDA1am5iRjF0NWxYc1pQQjJCamJxU1NGaUIrSHpkZmIvT2RLbnVJM1RjTzVtZzEwSjNldTRnSVhBNXZtVWd1ZVFKUWhSM3Q2aE5hYktmaGhXMWhLMjRucXJMOTVaM0F1MjJHbmxHNTlYZk1heGYycTAvR3JsUm15YU00ckJsNDFSRmMrcUIxNjlQb3B0ZEdldEkyOXRtbnVBUW9FNmJidjdWcVFKdHd5eElKRHdnTVp1L3dXOWNqUjZoN1l3NWd3YnliZWw0QjJYZnJzQlhxeXJINWpsR0RKTk5ndm12N1ZWUkxScThNRmpaR2FWS2NDc2J5WHN4UStod01PQWxXRnZCaU93UEduV2t2SXUzL0RyM3RYeDE4ZXR4L2FlVFdTUHZtbUZLVUVyc0NyRU1BYmkyM0hWNWtHbWNOYktoZmduVHZ3UEFpNjJzQVFna1pOQ0hhd2d5L2pqeFhvYmxQUW5NZUxqdmlCeHVyaXF6bHFFREljazk4RDZCY2k0Q2ZYSGQ4K295VzMyKzcyVW5DNnNnMWhBWnN4dzJ6YTlTelBBL25tSjhnUGtwd1BnVmxocGRyY0RtZkRRcUpEZFozdDZqTGcra2xLeDFndHhzczdWRm9aZ0lPcitzOGxtMThESHFlY3lZVkNkQ0p0aDBiTDdiSnY4R3owVlRXVGVqUFh1LzJNWmtKZVN0UzQwWStLZzVURHFwV20vRWJWY25kV3lHdG9FM2JyWlc3by9KM2JaWXRQTVhXaGhiR3p3bHZJSWxxNjRKdGYvY3M1V1pjeU1VQUxINmJPRUIxUWZONk1UaFNmWkxQNjBDUXFXSHdDcTdVbkZHYzBqVzg0NUZFTDNieEtIekhyU2NWVEtySFM1OGlVQ21lNFQ1cDJ1V1cxMlNhOTNOS1BWNlBCaWFLUVJ5ZW5CbFM0U1FOV0hlTFVXUlg4NS8vb0g1WXNWWnZhOXpYZng0Nkh1RHFtNmEvSEV5RjNEMWlDTFVCdmJGVElWaUh5aTJtQnhOdVh6SEhhSzlkT2pYYUtBK0hOV2cwZVBGdTZOaWZYeEhZNU9UKzB1c3ZjbnR1NmpTNGY0enN0bEx6RzdCS2NyMGRNU2dzTlFYMVNwMlR5azRWTUhzMUp3V1BEdG9kMlhtdm9ZYklzajlzY1JlajduSUw5RFhXMkJXWGdIMnYxcjdraVJmUnZMS01ZTmFNUmlNN3g2L3Z2S3p0SmxaRVRTQVpIcWczSnY0ditaYmlqRDIyL052d3NQMjdsM0F1Ulp5TitqaEZtUFF1Uy9sQzBDa041M0VXTmxkZnBzYzc4RjBjbEtwQTVlV1djWFl6VDdUTXZ0WjFkU004ZlpVMVorc2NPR3ZOWW1VL2o2bjk0MkM5NzRueUw5TnIyeFR3VXI1ajAxTFEzdlV0OWdTd1JwYmQvSWdXcHhSbkVuMnVsQ0g2YjRRNTh0dmg5VWVnYnNlblVqMExNWWVWdGQycDIraDY0OEc3NFcrSFpNWDl0dC9tVkkxYXc0TGZZeUUza1MreEJlUndUVVdwZzhRMXRobXB4SW5KaHJuUHA0ZnpFdTlMUDJId0RRL1VnUmgyQWpqMjFxakNiTkE0cHdjWEN5TkNkaEc5ZXlqV1NSRWhRWjdOZ1RHY1d5T1hrVENNZjVKNG1BOEk5VmJOV1JiZUl6Y1RFYTQrQ3BpZlM2MkhUbmREdVRYbkJseVpJZmppNTg1KzZqR0hDT3NvamJvWUl1L0FtZTlSWURHYmV1TlN4SXRBRDFIQ1BoaDRRTlJXa2t3bXFjdDNyN2FSZXRIRHg0YzZHa0c2NU5tRkhIa2k4aTZ3RVVaM3EyWldoRkxZS2ZEWHhFMEJGS1c1UUpzMUdnUXIrR2NaV0hkNDhTeDhzUDhrMnNqd1Q1SlI5azdEM1VWdWtOdXh5d0YzNm9DeWhIS0MrNE5ESGNRblZjM2syd2NMOXR4R3IvaERGWWJRY3JQdTl0bDcxZU1qMDNyNDBzb2FmQjdkeXRMc1dIV3dFT081VjZMcVJyMHZRVE10RDN1Smt4T3Z3dkt5VVNBb2NaSDdxVGNCbzkwMWU1STJCenJ6ZERQSm9mWEpoVFlrMEZhOExLQmdieWViZGhTdjdUVGZDeUlDNElEb3ZZcG85T1pqelZuaTNkaTRXaHIyS2tTd1lzL1Y3Vk1ZcTVVSnI2QUdaVU9rTUNsZnVVRDBnV3d3S3Fvam9YNXhxTXJjbGNNZ2NOc1hIYklxSXpFRG9DeGZvQ2JHN255d0V6cndEUmxRK1BzaFQ0Tk1FcmtEcFBvWnF4THBSNit6d0VZbnJSN1UyeWZwTFROQkZ0SFVqSGtiT25JVE9sNlFEbWxseldZVlp0S0txdENJMWRCaWt5b0l0MUI1UnlOWDQwWVZSYXNtRUU3dXJRaHZtSEdNbHM0MmhFQWFadFRhQW5hSkE4d1ZrK0VVRXRQaXcxNnh4T3l5clp1TWNaRUM2VVY3a3E2TTRGM3RPcDNrVEUvb05FUVpUVkExdkl3cExNTUtmSkdYaWtIbDBrZGs1QW4yTE4xZzRIdXViRWlpSzQzanZ2WldtSHJ4NUsrS1BLWGFSM2F2QmdlU3ZseTBkU21xZkNUU21zMkFFYXdTOERmZG9IdUU1c0dVRCt4Q0Nqd1ZTOVFlejRwTkVya09VZGtjbkxlQi9PU3Jab0lKRkw4WnVjTTZmUnB1MEJ1eWx1MzI5YStUNFg3NWxMTXQzMlVLTmpRRjc3VmlEempKSTVUUFFsdVVhV1FmWDZ0QUZ1bDVJeUIweTAwM09ldzV3WlFUVXQvRUQ1ZHpWMXExZGdGeUNjc2szM0ZNNXlQb1o3TW5lUlRGc2tiQlpCa0FkdlNpWUhVY1I4a285RlhTajJEeTRKc3BORERDbXlUVjNLaXV1ZUpqdStWSkFGQ3hBK2cwZlFIY2g4ZStvSk9pL3RJYVI4dGVxT2lwalBWNzMvam1kYUN6SGJESzZIRS9iaU9IZE5YekhNdXlYa1dFbUg4RVB0ay8rcDA3RnprR3ZrbXJKWElKUWhyNHl4NUY5MnFzd0IxQnl1MG0yb2VDaWk5eHFGdnNnMk5QeEQwRGtZbHBudldiQ2JwWW5wMzh5d1hEenQrMTRCb003WXZ0UGs0ZXVOcFRpeUJ6ZnAxRVR0U1NlWU11VHpjbFBOL2FvSEptY282R0JCS09Wb2NpTU5qL3ZyTGRoM3NocHdUaFVpeHFkREEwNzhRK1dNdDJhVE5UdjlJb1dtR0hSeWFwS3pPVlNGR1BEd2Z5NTQ5R2N4cE9mL01Gb1gwV21pUW1rczJHR1pqaWtJVFZPZGxVQ05obDAzQXJUQkpjZVFyZXhjeEtyM1VpQUx4MUxPNklPM053MCtrUVZuMW4yR0psN1ZyRmFldmJNWjR0ZzFLS0JRR1Y3VjgzOHBuTGM4ZEJwUGhBUUN1RTdjazEvUGx3dEtKZmUvdkowZFkrZWpKV3RVVDZrZU1YRFN0b0RxcDM5Tk1PQWRaKy8yZWxXbmc1VVVSUHpiRkNGMDNuOTNYc0xiSVlqZ0tJbFB0ZURZRFNNaW9rWEdiSXk4NCtGSFNlTWpMdnFtMVVUbE1VK1JBMVExQkhQMXEyZFBuc0twUWZQYVY0Mm93bzVURFhKUGFSUmU0NFl6UjNhTmtTcG83Q1J0ejBWTjhYOEY2dVR3NVlncCt6NTRYeHNhNkl1Z3hCazdOYW5Hb0lQUWlSblVxd3hzUGQ0MHdnUjFGWEV5dGh6RWJOaDlVTnVCM1pETWRiMTg4dExRY3RYWkJNV0JreXA3d0dVa3dTallpZ1puY2pYcU4xL2l3ZGJZRGZUOFdvb0V2OFVobHI0UmQ3d3UvVG9UcFBFbWszNEgvOVNLZHdQcE9Hclp5eFFoSFhscExVVmxjZ2ZyTnV0VVZKaDYxUXN2QVpvaFZXVUU0SkVnTFM3Tlp3MnRzVmo5dXR5UjJjK1RkSzI2eHpKaDJrbllhOVg4VURiY3Z5TFNvTk9wTU1ia3ljb0ZHSjArNUdNNy83U2YzaTNJd3N1N1Nabk90VFpHcHFpQ1hFNG5HRE4xNzAweDNzdUJKZVNpZk5RV1R6TkI2MVJYeUVxbkZ5OW16UlRnT2V0SE03aGxudGFONTE4VHY2azBXbEhxUFY0a0xici9YMEl2N3ByQWlUU2Z4OVlyQVNHc3cvRHZLR3RRVU0zMVEwLzF6N2dIYk1kSWxTNVBVdDg0MWhGa3R2clM0Nm9TYnFWNFg3dUFsWkx1VzY0czBoeUNaYVNrdG9OMGRMcC9XQ0sySnB5WUVHUjU2NEtpSFIzdGZvQUc2MkxETUovUGdYbnVRL043QmR1dlRVUFREZFlVZEpnR2srVEQrVGRrLzJ2aHREa3QyNXF5YjRlTzBQRDVsMGhTVlpNUlZIY0c4ak1rbm9DYXNXTCtiNzhMVklySlp5ZXNJeHJHamdMaTlqbXFzaTA3ZUpCWE1qWWlWV3VSZ2dSdVYyTTVUUHpXd20rL25LYW9TN1gwNWw0bTNyUFluK3B5UEcybDNtV1dxQTNhUkJzZDJxdTlMeDA2WkpPV3BINW5ma2srbmRIMWcyQlNiZDU2Y1JBemU0bDQrVmtzR0tSemZKdkFJU3ZFbnErWnpWMXpESzlBTXVMdFY5YlpQbHNPN3FiTGE1bG1JRTQzLzYwaDZJaThDZFI5ekdKbFB6cjdGVElUYVl4Y1ozVGtONFpEWGRMKysyUUFXZ1duRmxBSkR5L3JJVzhWbnpIQkJsWGV2R2RCdUdQaWZad092ODFFSnNVOEc3TGxUZ3ZKUzR2NEtyajR4MTlwM1lic1ZXOUcycWhGcXNHbEkrMitpdE9ici8xckcyTlhmWW80YmhMS0J6eWZLNjk0RVZmSkhRQ3FYOVc0TWFJMG83ekVrTGhmQ3lCWFJKSGUvRXNlc0trRFJueDdGVnQwMWdhTThVd2hrRS9GSDYzQXlpV3hJNFYzVy9QWEFqeGkwdnpIdlNLckZYM1pDMnZUNmRGV0VVSENETGNOajB4UllBSnhya3lMbnRKcU9WeTVETDR0T1dWNmtDQUdRb1VmMXBzRkRtRUYxQmJCTm82TVNPckJDak54d2xFSEN4b3hkMlVSZXJST3QzMTBua3h3VGRYYlJ4aWh2NFZrd0g5ZDVqRWR4Qk9mZVNvcWdpdngyTStodDJzb3pJd01GaUhaSkVlcWRWZHozK0lqbWU1eE1tUTY5Z3F6RDJnTGhsN0JVR2ZsT21Ib3N3NEx5TEtYSkwwRTZQQ1RDOGVzck9zNmRrM0RPL3ovZ3k5ODdiSzMyQ2VBWVBETlUreDBRdFB1TWh0cW9tZ1RSaDhGODdqTWpPeUNKWE5iaVFSakJyZ1pta2RXMGFHVXdGNVdPQk90b2o1cytoMmxVQjRSMEhHOVVYcUtqMGxpMW4xQWhTZmFlT1V1VU1hV21rOWJyV01SMTNLcDNJQmE0N05KWlpWRHBkMHNhUGc2Z21kVGs1WjZPeDFrMWJPOHUxcmhQVm4xRWFNdnVyZ3pXUFJqa0hKYjVZYVlBdVVLRlJFRG5CTER2eTRtaUI2L0pnVmFKM1NIdUxyczU5NFRsbktCSzBWeFlYZllFUkk3TElzTVlPSkprMzh3akJrRFN6TEJnOVk2RkY0Qitrd0tnWVZCRmtFVFBjZ1FFYW1HL1dTUUV2bG1qSDUrWGZ0cTV5YjExdTdpOEdQVm8vbDZlL2xCb0RzdElYeGdmaUZXTVJrUG0wMzhZRE93Y0d4azVPK0xMWUw2TENjYVAwUTY1K0FsUk5BTERZZitpKzNZb1h0WVRSNlJCNFViTmJZRmNWNEJ0bDIvYndQaVZOQ1VHSkp6aTBKYXhxMmo1ZTdSWUhnOWN3OU14bXd6SW1IL0ZiMjMzOGc4U3kvK3NTKytaa040dlhjdHAya1BnUXZQbVdlSHFpRHREdTRqVEsvQkJaaUYwNk1mTExyN1prdjNhUk13c2N0ZlBPYnpLVjkxdlJxTFkrZjJoTUpPVU5EVjlhVDUzNzR6amZ4QlBMNWMrNDdEcGZlbXJxb0NYYXhYSFY5RDNIU3ppN2xORjUvcVBSWHFoVzhhaHZZa2owd1JaWVBaSjVIRHVBcXArN2htSzdMYkhRSGU1NFVwaWlFWU9UbVF0UXRMZEt0czNUYWV4MDFWY2x6OThFdXN6WjdKSHVqS0NSUC9vRWVDMHFNUmgyWGxBOXdCNHMxRFVHeW5TR2RiOXpTTkxmSHoveU9VRStDUEx6QXF1cnYwdGErWmYzd0E1aXZxby9BYmFuWkVFcEpxSnRmTGtGeFZ5TDFScitsYXZ1cWt5djZENmM1WFhVREZZeEtFSFVXSEZUNm9ld3IvQlY3M2RiNmZJdmthcHZzd3ZiQ3JmNHhpOUpzUFBaVGsxZm1FS2VTdEZlRkJJaGJVV0I4WmduZlk5QlV6RFkyQW5DQXpEMjd0VTlPa1crQ0xZTDUwNFVrQVdkckZrNkJ4RnFvQTNHamZkN1p1cmZLblk0a1JtMHRHT1UxdlQ5aTJPek5RZW9tQ1dmT01NOEFxV0pyZXF3MHJiZkNCcXNnSFpjbXI1QlpiVjZ5WmRRcFA5bUdLQVppeEdMQXExNWNpUFZlZHYrRGJtRW14bkR0VWlVTzdFbVlKTGFwZFVLS0o1S0s4dkNPK08vQzV2UGt4U0RQaGltL1hrVWlwTnVaaXFyMUNQT3ZOekxJYlBpNUFsTGgwRmZ0MDBMdWVSZjdjQzhsWnBpSUhNVTF2RTFKM0R6YnBuL3Y3dTVSRkZyc0lNM1hhVTNQQnluTy80K1NqRm05TkdVQ0dKM1NMTENSUElJTmVVSWVoSGYwajRQT1hzUEZCRWwvN2wvMk5BUG5xWWVCRGluN0pjZGltV2ZZazRGdW1TMmJpalZESXRBSFhSenVsaGZ0ZFRnQ21HTnpWK2hTUU1MWGNJUG94M3V0dFlmRTcwcHNPMTZqVi9lU2piNzF6QXIvbXZwTUUvaWdkMytQTnJPWGtGaEJpbWtOQTAyTEwvNS9UeEtiUlBNVmVMelFrSk5OQXRFbFRQZTVEcjdnQ3RZQTJxUmhyNExtMVg4blRhRm4vcGNFQnRsWFprWXFhRUpjYlU1SjIzSmluVWNUOUhNSmpGaWY0MVFLWlZxcVZTdTVUNWNzUFBtSHhMQ3lnTkkrTTZ2cCtlUGFzTEg0V2M1cWFYVkNMbVVXR2lIR29WMC9DU3lURzdiUmlCcitzNENhdWxtMVdCdWJyUWlzMndJNkRSdjkxQ0FDN09HYXZoSDJ6VTV1VUx3ZlRaZWJ4T2VEeEtwTXJpNXU0eVJPMEg4b1kza2RtYXh2bWw2bmZNUE1NQWJxNGFaUkVnRFBqUUNYWmh6L213Qks5RFNYMUZVbDNLMEZFWWYrZTdzNDYrMzRaaXh6NkZidmZsZkZncUlYR3U4dkZHVnRWa29KSlNNTWVOcVRaSW1CYkxDa1A1QkhIUzZ4bml5bmZldzlGdGN6ZHhVWEc4RzRka24yRGtSSmtqZnRmUmFHenIwK054QVpBWTJadEJYNjRvK1lvSHI1MldQNVJoOVNZQzJ6aCtDdzd0bUZVQkJIUng5YmhWVEFjK1UwT3ZjSDJkWnQ4UjArZVFoWlZQdGljVjJmNit0L251bGpweC8rc1BFSnhCQlhuWHI5eHlEa29DN3NFRFJiYm05YVVnNWFjRVhyYlRGTGpFQ2haS0lwSXJuRkhlRFVpTkp0Qmo0QjFOTEhoRjlCOFNSVEFjdlphMDVuRUtlK0NpbnV6NUwxNFpmZ1MxdHFQam0rZkxzUTZuTzhnVkcrUDAyTFBPR2g1eHBvem0yTnd5Z1c4VXIwaTR0SkdyaXNDNVVIenJITmxkZUUzd1ZWM25OSlhkNnMyQ2J0NU9aaVJHbGRtTW9oaWVHNStMN1M5Nk1IODU4d1lzbi84RVoycDVMcE9kSUZETkNGM2Z0MGdKNkZNQnAvMHlrWG10d2REOUp1Y25Hd3lZRWFnblQyYTVBam9rRFRibUh6OG8zVWs0Ry9LVEhmTm5rMkdJMEh5b2U1MVpCWVIwaml4OGJSQXhZVlphSFh6U1creDdFOENkL0FwdWw3ekd4Wm8xdngrWUZNMFFjMFJOOUd6Nkl2MHkyNUsydmVXWUE5SWxpMlQwNHNFb1FVdC9ySlB2RWZsZkZoRVRicGJMeU8xY2tDMzF3VGJ5UHV4S2R1TXNWVzlHTTRXSTNTK2RtK3JUMVFMQVVTbGl3a2ZCMmF6NmsvZ1J1VVozVGhMMnRyc3JpZHJsK0p5cXk4dWQ4YlU3TlJqMURRYnVaSTNibVl3dmFySkVkVWQ3OW42Y0pYT1JrUkdIWXl2ODg5aTN3TCtpUHdVZWNwakVPcndEQTcyd0luQTFiOUVFZ1FzSnk2djVQNlVBdWsxT0xuUFBDQnFEK1pxYy8wNFFXcUdYNkZsblZYdk50WDRBZ0xKVUZrdEtQS1VxOUtZa3BNTVBRbTFWK2xnelpKN21kMm5pMU5XODZ1dGhtZS9MUVpyc3JUY3lMMTQrdFI2amMwWU4xSlFkVzBCbzlyakc5MHFkdDl6d0ZFZ2w1Qno4YVE3U0F2cCtWMmJQMDJORHR0TjdmZHdJNW9wTXpLRnE0SGxkL2lTbGJQTmFkZGlXRURSMjg3SWNYNk1vTU1ZUHRPWTdzRUNMMkhHbklxZ1ZNZm1NdW5iRGtpT2hxVEl6cWQwSlFWS2tuVks3R3J3dExJbmJ1enc1Q1hRR0F1L1lYM21IWEo0M0V5VitHNHU4OERDTjVwcHpwQTJYMGw3M1U5UTNveDFFVjJXdkNZQTU4L2NpeFpHdnBXMEw5Z1ZzV2NzeGFxaUVKTHA2Wm1SZWIxVHNCM2Y0bEFucTVmaG1KdVBucFdTeEpoblhlTUJ4SHJhemc4bjRsSUFrZ3dWdGlkR3M2dDFLWC9qWEFvRW95bDN6N2p1M3dsZW9tN3pNNFVzZFNsbFBhZnFhNWhaalE0amhZaFUxYXpjMHhrT04wYXlLeFo0VTd1eFR5U3F5dC8rRG1jQW5SZSthNXd2YzVOMUVKL1loRmk3NCtFdGpBOVZmSFBsdzc2K0paR3RJaGl0NmJPK0MxalRYQ1BWYUlmUWdjSkVhL0IxYmYyMUJQdlc3WGl6QTVKbVdDK1g1LzkyK05iY0tDU0NQRGVnWElmakFicFRSaGgwMURXRGlvdGJWazhaalMyNEI5OXlPU3AzODdJWUJleHJmRTNhTlFiRW5HK0R6bVF6cFpMTndqeFp2M0lETmN0NkxDWkluYUdPQUtqRDdwY3h5UUhPU3owdFlCMy9iLzZLTmNsNnBqaGdDV2lQSXpudHhQR1pKd0dnRU1rZG4zaEhPUDNCV1F5TjczbDlXQ3BidmVBalh2T2srY0w1RS9ndVhubGg5aytnR2FCYkVkNzlYUHNGc01UYnFqWHgrYnNGR2tyVHZCQWtkaFpyTlAwMWNWeXpkZW5wVkNIS3diQVlzNk5xQUU1bEpJS1JWM2o5R0hJUEprVFQwUnZXeDBkU0lSRkNBT0RQL0pKZ0tReFhISXp5WkNQbElWY3pYUmluemVPZzZGc24vQ3ZPNHNMVVpQdHBLZnNJNlhsUDFnRlJRdllmUWdQK2FMa3VSL2Fmcjh2MHZNdGkyd3dWei9UMGRLb09Ja1FScHBoYUVHVTVVMVFmY1B6Tnd2UUVoVExVWWhBVHdKMnJ3bW1BMTZSbHdibmhwTlpzK3BPdHRCcU5CS0hlck5OV0tGM3J1NVZBSjhZdW1YY3JvZDFCK08xTjk4cmMzSlY2K2pXUUVaSHd1QXQvT0EwMDRsYmZqM1J3Vk42b2lLSVV3U0c5dG13RHVWQXo1R0FhdmtpUnBXcXc5TWFrMnpyVUJ4d3FIQ2ZWRk9RcXVhekRBQ1NJQTRpeGVxcENBRW1aTHNpa3BqMXNuYU5SWEY3K2duZXJSZXdvSTQxRkcxQ01wVXl6QmtVZlNHcG5NeVc0ZjJ0V2t1M2NTaitCd2VWcUtXdnJZRVNXZTA2QU9nQzhKbWpsUlpMaXM2aERIK090azV4N3lXaVpyUkVKUWt4UElhczN4WU93SkMvUjl2Ny9RVnNlWDk0ZkVJMWRZM00xc1dPUEZmMTZ4U2RxRXNCRjJQbFJQZDI1MmNWTHhLMUk2ZlpQZmVxUGNDd3ZXbFkyZnJUc2Y2MFBhZVR2dEhBaTJUUVlMYVVvdkl0UHN5dldGb08wajl2KzFubms4SVROWFJ3bnk5WE1tbHVGSVNCakpvU3JzR1l1Y2dxaHI0a0ZQMG5BaHo3T1A1TUIzWWk5M1VTbkhMOGlXa0VmK3ZoRVZaNk5td0FBYVVCQk5XcU12cSs1L0NPVXN3M2Z3a2dKcVJUcGJmbU9qUWpuTGw0U0NtTWRRWm5TcWUveXJtQmZUdE9rV3VySU1oK0JsUjFuOGowbnRjekQ0ZS9qUEh5ckw2RzAxNVo2NlpySmxuZ214UEl0Rm03Z3J6ak11ZVg4em9BN1VDRE5DNXdranFJL0hJTHBIVjVQSjRLaTVFd3dPZjBxNUVCNHNpbkdZdnBPeTQvSm5lUHlTTEJQeXBqUVRIUlVrR00xWmVmRy9uajFwQnJXcW8yUkJoWjc2a0xEMzE2ZE9DQVg3ZGtxNmw3TTRldzg1b2RLd09TZVUzeUxFL2RmblBoZHJxbXZGY0lCZ2tBSmhWTVJiaVFSWjlONHZoa050NkVxdCtCL0NPamNoWkFWKy9KTXpCcUpjTy8vd2UwSS9pWjljQjh1ZHdraDNSYjZFZERkNENPbDVQczBXRGZOTlRsN3lOL09UV292c3F0SGk1bUcrUWcwQVZIR25rWXFta1Ztb2lMMG1iT0VwaSt6RkdGMGc5ZXFlcjlEdnplci9ISG4wMnFFbWV0eXhsd2VJZ0dPYXFRdVR5TGlHeFl6KzFacEx6OVp2WHRGL1FrUy9lZGUrVEZ5MG1Tbm42aU8vUEVjRVZaQ0NCRytIaW5zY1BEZlJmQk9PS2FDUXoyMGdyVWJSQm5iOHdLRDVyMmhhL0F6TzkvZzQrc0FBRElieG92RDlPb29yV1hRdFRQbVNEWXg5UHowa1ZENi94d0VxNi9YbkVZS3VjQ3R1Mnhqd0dRWFIzM2FqcEhPZ0lUd1lMMkwrVTZTNjZSN2pOS21ZbWczMStwWklldnI5OUFCM1JtTXhzVE56ZyttUFNzZkZhcXp3K2hCeU1mS0N6WUhJeU9vWS9QclB0YXFhTGt1clVZaUhiM3hNTnhpcjdFeGZ1TmJkYUhBemNjR0dYZmdsT1NZNnhXd0kwZWxFKzJ2RU1NUWxoanNZVVlGVE4rWG5FZGRtUzhsUWhuWDZBNVdVd3JSZ0hNZWFMV05TbFpRODFtRzBiTE9KWW9mZXJKOHFRMmJzdW93dUZuYzVaTEFWQmdubTdzNC92UTE4UWR3SDdyZ1NuOTZtUjVrNkdDTm9TaTRHaEk3VzhJeERyc01SNDdTN3lKdDJHTjEvNjRaRkFmbkV2d3lKMG5HaSttTWh5NGtRd1BaWWZpRjVvODRKVEN5VHBpM0FhWnNmSU9QU3J2VXJ2VktVd3BDd1lWNmszWlVaUUJKNXdOeEk3N3JTUG1VRTBUeVpzeUMrYlR4UWczdlQ3MllHWmJ2cHVNVkpwaGtpZm53elc3RFFyYUdqMFQrbUhXQ2FjUy9CV1ZNZnRZMk50NzQzN2JZbzZueTdVWWE3NEVOMkVyeXVvbFJHZ1JkL2IzOUM3TXBsblVpeE9UMHFJYnliSldzZ0gzUWE0UjNRZXNuNHZGNkJURmdTZlYrMlBkb1hZSUo0QW9HRHk3VG5ySVRLRGlVcUdQOHE1K1VWUk8vMmRnT0tWUjZzTmplaUpWdnRoUnhjdEdFQVVHa0FoUlNkNXpLWFRRaE5xcVhCZkIzMnJFbEFvaHdnK0tnWERzeDJ6azN4Wjh5WTczTTV5Q1lSZ0xWSDljV0pINmoxeXhjTnRQTmxBMjJ0RUlCOUliQUNjUDBiQzJha0MzT09LRlNsN3hoWGJZNmU4TWlPQVVKU0hxNlUrdHlkVzk5TXhRZ0RRdC91Q0VFaUJZWTZjYmtVSGdTL3oyODBxZk14eHJzQ2k0S09rK1huTUxtNkdqcjNTb2NwS1g5bEtjRlhjNlFvTGYvcmZ4cWcrM3dqaVI4b3A0TTltSmNFUnNCeXRTMmNHaEtkdlgrUW5mVTJ0bUtyWWxMTWRWOWRVUjRyN2kwV1FOTkE2bzN0eUswNmt3S09XYVNkOUdUaGMwNjZnWnp2cFI4RG1XSkJ4UTdMOVNNQm40cDZmS3c3T0hrcVphNTBnZVZycXFJc2pjWTVHbnE4dGZldTJEK01UOVg1ZExqN3RvZ1BaaE5WKzRJUkowRUxqY0U5eHRvb2NDNGsvbGxoQ1lwSVRFelBRUlNzUzVyVTg2ZHNVWXI3eDFlV3NmR1E2dW1Pa0JGZEVTcndTTmdKRThnb2V3eUR4WEpUM0RxMFJXTm5lNm5kWW10YlQzaWZSbGRIOVBveTVTRStTbkNzajdwck9PYmtXMC90SXY5Tm51Ull0Z3h2ejFFczBhNjErQUFkZ09GRWtTK3B3c1M3NW01dVZlREFDaEJwWU9jUkVKeXpyR1I1T2ppSGpMa1R1L2I0RGhYKys0djJaR0ZVUjB3ZDJYRnRRZDh2QTJReHRGV0IxZHBsZzdHVmZkQWFpN2Jvc3V4VDJKTEt5bnBRWTB1SHQ2cGttK1hwcWpjS3A4ZTJBc1IyWlZOcnFqb25taG4yTnRGWTFyWTEzUEkveUREREVUcFVDM3NsQVUrcms1UWNQa2I5QnZSVXBteVJ1OStjTys5MVp6bzhUWkxFemhubDM3MnFEREFRK0tRdVhGM3RpQlU1SWRaeEsxY0p1dndrb08xMU1RVjA3T3l2ZEdsZi81NFA0OGtUdnQydXlPTmZHUEg4eEtTNGlwMWRqaHo4ampPUWhoWGNZMTlpYTVXMmk4TTV0RjBTaWZlVnhOdGlvaC9MZFNQbVcrbnNqVzJ5U1FBRnVqb2l6MTYreFMzNW84Sk9icHliVGFlY0hsbmNHc2gyS1NpTEVaK1c0WEZ1OWQvaHB3VjBJVXBDTldWVlZBV2xyOTQzVnZKYzRzTVZjLzJQMWJ5eXIyTUVZMkQwYjQ0QXU3bEg2R1VuU21ZVzNWRHFvTm5OdllSMVQ1UmNIUGtrMFJwekVSRmNrYzNrczdxR0hmaVpFYWVqU0tKeXA1dHBBVmtpenhjNTVHZm5tZk9DUlJJcGdKMk5qKzErNHpMN2xlMHFyVm1KZGozNktoVUFqRUI3MDNIRWhkdndBelB5ckhmZ0MwRGFrdUh1ZWFnMCttZlhCd2NlajcxbXlRdUNHYVM1Wm05VVJtazJGaS9zT1hmamhVaDdNcDBGbUVGTTBTSFh1MlhnQzFMTDRrSXlMa3lzaWZnU1RwUHVTKzRNdlRQcW1aQUNNTitPSkNXSWJpZTdwOUNsMC9RZzZUeXlWZm93TjR2R1dUc3VBSmpoTllZMm1uQTc3UEJ4a25TaXBsZU93cWVrZzd0Z2IrdmdjK2xhNy9PZWRlOXFlNjUwMlYwVFNKTE9BUUtFVTRKQmFEVDZOWlBFMzNERzZUVG9zTDRzd3gxMDdlL3lpZlhoVmRmK0dMZ1JjN3ZxUFlJMDRxaU9hWjZUTU5GWVpTbnR6TlFyK1lGVHBKTC9ieFBjaVFNYnZKbUdrL2dsaU85WlpvWWVqaEkvMjc2ZWhzK3F5dE1Eb2p1MkxIaHpudTNsWThhQVQ4L0NiUzhCaFpOaUJkNmZwUzF6d05DQXh5NGNBZkpib3hZY2ZmWTZZeTVRQzFpdzhtSzA4ejRzOHZtNUgvMjRCakFXenJPb0NQVFZpWGtRZFBwVEZrbDk1dU1tUG5Hdk9QdWxCbjVTN1pRWEtnTFBadzdoZVkvWDNrOWVEbjgwTm40bVJ6aHBMN21rQzJRbGdhNUE3TGQyQTJDd2tWNHVhRERoOENBWHNzRnpoRmVZei9mWllua2YyMjVmaWhaaXdGYWRRbWtVM0x3UkJZM0tKRFo4eUxPYnJjdUNvUjNNSlFmM3BiK1RTQTB6UVB5U0ErcUEwOXZScm1UV3NrRldEaHhFUzl1cTR3OERIME1Ib1o0Mm1WdUoxazBUTmg2clhoQVk5L2YySkxsUEFDMWkzc2IyWm9rdTQvOXRpaS9SNEdxRVVmNTlNQUlnQm80TWxUTnFKVmRwY0RBR1BRNVc4OUEyS3Qzakt5S0JTdTJqU05OWmZvd3RtWk5ORkozVUFRMFBsUEQ4Wk5ZWEwwb0FmQjlPdXBvQ1YvQk1xRjY3YkdQUk9pWEdDZ3J0djZsUXk2L2NvNnduK2tZTnBXb241UmozTUc1V1BUakRRRmxHOTE4MGdrZ1BLeWFrQjluQkdvNi9ONjJLM1ZldThhekwzbGZQWWt0YUlibHhSUFhMTCticWlPSVNzajlpbnVHVTlhb0VFcU5MWDRvMFEwcHMvVC9VbTJiMmhqSXhIaHdwYnlxcU44MjJLNjdHVHZiM3hFblFrYUxJQVk5OUxBRG1CNXV4SWo5TlVvTktKNEkyZmR5YU1WWjhWV3pjaXZ1L2dKVDE5eGV1ZGdNUnJCTHBGVE5vRC9pSGtuUEwvbG52Vm45cG9kcldEOHV4czNjNnUxYUVxc3IyOG9JQ2RPZFB2WDlwcGEyR0hSREowbkhUK212QVZDdmJpaHJaejdkNXlVVUd6ZjlpTkVHTFpLRG9SR01YV3RyR3AralFLZVhzZ1RMVk9uTkJrY3JtNDVGMVp3MWFTWjIzWE4wRlBpVnFveFpka1RQZDFhWThGMjg4QzNVU1QyRDE2MlFUOXpPdWE1SnNMV2k2QlBaRG4yeTNBMnlHTHpIYVk4WThhaEtNK2dpRlF1NnduZnRUUmxsUFNENDVNVUdzMkFoK1lNdGdLSElFZjNUc05VeFVMTmwzU29VOUpic3JWby9aTUIyZ2NFc01qWnZpakVGeVJvZXpJaGUzQm1tckFhNUJKWFNEM1dXbCtsdW05dTF4MHJSVCtuZFhSQWs4L3VyNDBtWjFxTUlqSHBXWmk0VVkxTU5BVEQ0dHBtT05oMWEwQ25raVgxd2M4c1RLT1c1ZHJJbi9NeW10NFpxWmgxZk4wc283UGpaaFljcU4ydHA2clhlOWljNHpZa1o4SHZxU2w0ZExCOFRUeXJnMDJ3bU41dWdEODYwV1J6Yktkd0FnQ3ZLZ0J3aWdqaUREZUNPWW1uMW4vclU3cTNNV0ZzbFlRUUgrNGQ2YWp5QUVoendqeHFJYWpQUFlGZDlISWtJNjNuS3dvMlhVUURqdTdhRkhxNzRKQjM1UWEzUUZRUGxQOFlodTdwRFd3SGY5ckswdTgrSDFPUjZFWkFhWlZPWk10RzdNVWY4SEE1eWZTWTYrYTRyVWt3MGFkQkhRMjZCRndKZ0o2Z1pNRkRFdHNkRU1qdVhLQmtCV1B0TGlWUXNncjQvWWM4MS9PTllDV2w3Q1FmbjNGcEs5UVVrZDFIc0ZTS3Y3YXBwZWlHcXVrYWorbEU5dnlmU243eFZKa2F1OUJ1YXpNdmpFa1l1T0oydzhwZmN6ZFU5K1ZkWjVUOHNUNDNZNlVDNGRram4rNm9henlHYWVJU0ltenIyeDJDdUlybVlCeEcyWTBJQ2NKK0RSdEh3Kzdtbk5PQnVVeWJ4WU5Qd3gxZ2E3MGV2SXlWOWJNZzdPK0NzVTJ5eGkzWWtVd2pGamttVjJQR2o5MWdvWWpMOFpCMEVCSXk0UkJtYjdOY3lrMU9qRHMxeWpBdVBuc0ZwY2RRbXpKUCtZa2p4Mmt3b1ExbmNXRUZyeWQ5SHQva1FpTnpyTzZlekFjTG05aDNKeTVDM2laVlYxMXZJeVl6NThxMSszSkhRRHhFK2xjSGV5SWU3bTkzTVIvMUVNOEdXbVVpYjZoSEI5YmtmQ0t1QmN5eDlVbHFtaWRtbGNNSTZibGxuTDlZcDBaT1JhL29IMHpMMzVJaWc2bGZGeXJWQlJDTlQ2VG54NUFSaVZCMTQyVllhb3pFc1FkTWZaU2FFeHA2ZWNVbVJhc1MrbjBHSGhQb0dBczBkeG5LODBaMkNGbFBIYjQ0akozdnVnNE1SZkd1TmdoSzNtaVdmOVRvbEVzUWxLYzVPQWJieUhEeXE5dGFGczZYYWxOKzAxdnY4Rm9yWEdTY29vVjVrbDJRY2puQXVCZjNkMnFwbURjOG5KVG4xTDAwVlZncEZUbjlKMkVTRmFCTmd3T0dlZXFJc1hxWFVlVm4vVkk5NWhNQzRKVWsrN1VXSEk5QXB6QXdzbTlOV2trNXREa3VBazd2WmZYVGFaZDFrZmp5SlFVYmo0YkU4WkQ4Q1I2bmREamZ1U2MxVHl3RVdjR2pHS28yRFRFaGsvRkNicGVhVmJuZi9KY3lIcWxWWTM5L0taUGFBUlhoQ1ZubXJ4angxVXhxTHduNExySUE5dEZoRHRkZmZGcHAvRUx6dGpKVzVwWnBtQ2wwZWVUTmNjTVJyYmxBRXh6eGx5WVZBNzRYcjROUmdyOXZJY09JTEFhUmZoaXd6Y0JkTmpzVmV4cEtmWEZFOCtHTGdlOEs2cHJHQlpheTBadXVFQWFlcklQVFdhTzFndCtaVzNPNnZIbklOSVo1QWVwYWMxWk9MMnIvZ2V0RU1KckJsRnF5cmt5eEdOOHc4N3c3N1VOZmhzUEQxWThGTUluLzlWM05qd1pCTmRoWlFNQ1dzcWJQSUVpSFU3cUs2Q1NHZHQwY3BUUk5lb1RYVXkwNEtIYmk0cnFIOVRSY1FXd0l0RmhXTlMyRFR0VFJhOUZNdjdYaXJjRG9DNnhvVm9Bb3lLcTFtUDN0UmorRUp3bUd0eDhBU00zWVUzTHd2U3ZuM05qaEE3ZDhtYVc3S3RXUk93aVFWQS9JQ1c3dkx4bVlQSnlkMHpKcy9TZ1ZBYnQ5OUZSNmVLU2phNGw5dHhUdVA3eFdkYVo4VGhyd2dOSmhteEhHdjRiMFAxUkFaVzBrMW1BTzUwSnRaRHpQVlJ0S01DOGVCTnlDeTEycmk5MnNUeDU4eWpPU21WbE1MbkwyMUZWKzlqMGxYYjNFUEc1MExRZWduWEdSMHA2VHNXanZTU0lzc0ZRVE9DT1pYMWkzRGpWakpVTzl2ckdmTkQvVkxkaS9jM2xrdnhZMzU0M1piMUZJLzhUU243a3V5L3NTVngvQk5paDdFMER2YjhQcjZHMTMxdFVreGhSa210amNXWE5zZ0s5RlZPWDgyRXhUTzJXMW5VeGpaNnRWaHY2WTVMZ2swWWo0Vzgvd0F4YlNMbEtlTkEyV3FLazJOb3E5QysrOWpKNy91eVJGMWtRRjFHWGltRnhnMmlZWnJnaSs5ZytraDRSWkJuNGxxMlpMY0VwZE05OXFRWEZGRkJyL1cyK2x5ZUE1NTZxU0FtTEJsdlY5emVscm1tMjVBN3VQaDNISkM4b1oyZnNIZmdmV3gzZlY0QWFlb24rZFdpam5XQ1BnVjJqOHBrOEVJQk1sbEVFcVViejY0QW05c1BjNUNsOFRiV3IrVHY1ZE42aHB0WUVBdDM0V0ROZmxVeURvNnRTZER2QUtSYXBnMFZhZ3F5emh0TXI4VitJQ2RzaGRYMlBaS21qRDFyRXMrMlBXMVYycjBoM2RURUJPZ1ovYzVhZlVKUWJmMVJyQnppSUVxdUtMUHBYWUY1Yk84UU9qN2xDaXB0SkcxNk1tTGs1TElMZ0FNbkpxM00ya2FGbkg0VGxpTUV0VDRXeUVRWEthaFNHaVFEcDBNNVo5ME8zR0NGcWFFQ0ZZZWVUTm93SlZFemE5VjR3WU1ZLzNUSGN1Y3V4aWdJYWc3ZmJXQmRSRFJhMk1OaHFLUThzYWxyajg2THprbUxlQW1xRlpVd3RMbjRqL3MyVUhPT1RCTU5qaFdwNk1yejNiZ3hjTU9lMW1idXMrbmlyVjM2VVFDOU1nRU9JR1pWNWhRYUc0UE9MMzJ1ZWQwdHMvNWhMeSt6bjdFNHd0amtnVTJaaTNYNlovekhya253Q2hPTEFjaUkvY2VQTzMrc2hpWTFNZUgwelF5SEtDT203RHJrQ0VTMm9OMzhtQll2SlN1MFRRTlFFRDNVZGlFdldVSGtFSzMwblFyL2MzQWVXdXhrUWZsakN4bEFHNTQ1RFlBZTQ1bldoU0RzYUROMDFzOHgyR1o1RDZ4UnJMVEo0VzQ4TTU4cEdUSXM4SVR6UU50R2dXUExCNGoxc2grQzY2a1QzcXNNU200dFo5NTlJMk5oL2dhR2F4OHBIVXZYSXlRdFo5UUNqSlAvK1E0cTUyR2J1ZWVVMXhxdS9UQjZYR2pqYktQODBEQzdCTTk0UTJrbjlTUVhqRThXc3AwT3VSZUUvdVZtZFpHd2p3cFdOMzczS2FVUllidGdaTTB4WVhhTjRRUkRmOVBCS0x1M2QrbkZkQXdieUk3TmhYNENZSHlzRFRtV2swZlZFcGtlMUpZTzE4NnVTSVRrM0VDMHplMVlORzhpOWg0NDgvakMvbis1dCtpMUk3QlBjeFpuaHo1dTc4Qitid0NQMU1xbTYyWXhGd2FMRENuY2tjcWRsUnN3QjFLODYzM1JCQnlkSEwvQXo5QnFqSjRRZUVubFEwQUtNRzFpL2c0bHBjOVpIeW82NTU3NlVZMjNwTlZpdDlKTnl3bkxrWFBWNkwvWmgvalNXak0vNlB3VisybnMwYjAvVzI1OUp2dWh0KzRBZWNhRVdPUHRXa0NnTFVvS3h2bzNpdHpjS3ZvQVdJY0p3N1p5QWFvcFVNRHk3NGFUd29qeTljaGFJOGN5QnV4WXlkQitocFVxSXh5MXpoVmNTRG1Ma3h5S0tSRWlnOEZSQkpJU1NjOUZubEdScTc3VmxTZFVRMVFwbFlJaWdIUTd1NlZSTHFiaEhhVkJ6cFJTMVJpNUYwWTdCYVRYY0o5cTJwdjFsRlhORE5kaXJ2YWxFeFNxYUh5TWN6K1p6bG9lM1JiNjVsUmNGMHlwZUkwVlRKZEZoUzVJQ09jWnVIWU1TMS9pVW5ENzNDclJJaHZpK1JMMjk3UWJWWnFSekVhaXp1emxSV3gyL21aZFFqb0w3LzF0WnNrYWJnb2kyMkNwd2xNUFdKOEE1NnVsQm15RGFIOFg1WXFVT3BkckhEdUphQytxUXB1bk9EMVdNMXBrN2dzTXMwK2VRMVRPUDlFQis5R0JBcldQL0o1ejRFTnRIY3E1d2l2ZnBVMDRXdWlUUk9ZSXNpS3h5Y1BneGhKbDVoVVdwV0dDNGRxN3NuUm5ZOENMazdYVHk5cHAzTnZyNHkvbVJHcXM4Q28vcVlsUmlYbUthM2VrRHgyd3Zsd085Ty85VDVHc0FDanltdlVjYThQWnlEQ1RvcDF0ejE2ZVJ1dC9UK1B6TmFiRnN5T3VDWXdHNjFUMFN4WlFRV2lXOHhLWnpqYzhiS2VyRWV2NW1JQzIwNnM5aU50Ym5kcmZIQmJuR3BEMnNWLzViWVJmQS9CUnF4azdGcWxvM2lvQ1NzN1A0TXVkNE5FcS94em95MzY1a2JlcWljMG5oaW1qMWl0STBzVjFxNUpnNjlsM3VrSFlGckxGdVJGN0FETjMyYTBRaEpOTnRqcUE2b2Zsd3JTL0RHRmt1WmVkcWhudGRvK0RnWU9zOEU5NzI0V3dGem1vcC81UnVLMjhIRFNYcFprMXl3b2d5Q0J0NTNNR3JoM2J4K2s0a3h6UGpSKzI1eEVKeTg2QVZpRVlYaTk5ak9KdUJ1dTFwRnRuSEFTdFdaMnUyOEhndkVvVlhyZXhjbStTZjV0S0FOMWFBdnUvaGpJRzVwemdKZVoxSlV0dmY5NHlWdVRmdVJMQVNPaGJvL3NMMW1RQXoxSk5OekJPd2h6d3UzT0cvcGdrQWI5ZkxMN210eWtTaHpxTWh6aWdyOGV5MWFxa0FBSWROWGRlWUxFMWpJZG5qdklkcFJVemhmZjY2d2pnSXNiSEtRU2VRUDFvdGR5T0tISU9hUGpTQ0dzVHJ6Z1llUmNFb2NLRVZHeDRBNHlESDc1ZEE0K1J6cnVBZFhCSHZhTGpBVDFWQnRMQ0VmOGsyTnQ2RFJ0SGF2blorc3R6V1VFTEt1cUxsVUV4VFlpdVFIcEdBMk9sRDFoOHNTTEtvemRERVpiOFR0bks0aHdVdEJFVzJvQmJ2NWZVNGJPZXUrMWZQUDBvdVhNZkJnU0l3b0Q1TFRHc3hLRVZUWk9sRlRCSGd0OUpJTWVHQUdLeHV0WWhKMStaMWI0TmlGWU5kdTNFOTU0YlQwS0lDbXNhbXRJME13Y3JQSi83M3NCMjAxcXNUN0Jwa0w0ajdrcWM2ZzF3eVRrVG1Fck96cGRWM216Mk1QdlpHT1c1T3Nac1kvTTBpWnZRUXNKNzJOQXpoZEV4NmxDZTIyZCtMRlFFY2dWa3pIZUhFVVFJNzNNV3J0NXg3bitsSlJzYUg1c2lPR3V1dFUvRXVrY3BxZG1nTkpFQTBHb1Y3SFZhOXVPSjVXTThYc3dQTDRPc0FYZW5sUENXOGtlZzZYVXp1VXZLSXdmTmpNYkkxamxBU1RPT1dyN0YybUcvU2l0NXpadElWb1hvTzdNc2Jta1dNZWlDbHpqWjJMMnQxVUdlTENzZHA2ajBqTXl6ZFFKdThKQm1EbzRJYXBBNGhGMWZXTW5kcEkvL0dvL3JwbExtbnh3eWlvUDhTa1VtajNPbmVsajhrRDBEdFlWMWYva1R4NmhaZTU1dmxoVVNVWGxrTTlSRUJvTFdtbjVUQUJMcEFSbVBJNVRFdkR1aXNvcklpNGJJbUNtY1laVmcrR1RQNk96VmliZncrbW9WZi9xUEM0RXYwOVdURXdlbjVqS3Z2VXN1RThjT0p3RlpoMmpaa2ZQb2pvcyt5M1h0eHh4RWhrbHdBOWtiVTZvVHJhRWxZU3RCbHpNQllERjdJSFJrdWNDb3c2Nkpmc1NIQTF5RkM4OStZay9Oc0RiaENpcHlQakp2SzJiSWlFdHNLYjhwbGFTYkVsQjc4dkxNaGdVMU8rV000WlYwZDBheWkzbXpaVi9UcXIzd3VOSURMVUgza2NOY1FjbTJJVWlLcldUNi85ZUhsd2JDUE8xMnI5YnY4aEdrelBkYUdVcW5uSlZUVSs3R0UwY0RpdHBKUEI1cCsxck9qa3lnNExQZUpTQjJaVEpsazVaTGZnMDBQbXkwWGdWUVlHRGJHQjhDbEFQOW1IR0lmb21sV1hXbWQzc1MvREZQSFZtc0RveHlPdzIyck01LzN0NkFqOHcvMm12UFQ4RHJaSVJzcFlkR3JlY1BJNWI4b1d3R3dtMXo5VUNZaHlLWjFNZEhVQi9mZ21RK1JML2NEbUVBTW9MTEQvODZnQmczbXR6SVI4TUpyaE9EVi9uY29Qcmo0RGFpbEhDS3hmay9HNWFRM3JHLzNBSVJJVVRaQnNuQ3h4TEFPOW44VDlXeTVJc0xnenN3akwvdG1wRWRUSnhzK3NEbmdnS3BSYUpGaDdFdXJzMXo2dG5heUpCTDhFbmJEeDVlRWpINTVILzlRYWpJWWo5ZHk0ZDlqa01HQytta1J4c2JEcWUwZTYyTkFnZnlYYm1YOW1USEpWV1pWeTlYMW1WYS81REJWVVAyZy9FQmtKODVQQkkxMnBtL2twckFGOHVRV29Sb2ZKQmZtYjd0d2sxcEVxbjBqKzhPVFdQbDMwamdjOEtmOEZ1L0NFNC9xRU5FZGtBR21CNWF5ZUg3YTFOejVzMXUwcGUySDVwRENCMHNIbHArSXJkVWVyV08yL2pNRWVKODFRV0t4SkRmZ2EyMnNTMk1NdlpWZ2grWDQrWDRkYmhCVlR1U09JSDNlRUQrYktpRzI4UTRCN2NSbjN4UWN1U2NVejFTSjNmZTUrSmhEUWZJeVMxZlZFQys5NU9lNXlIQTRRRGF4Zm1YTjB5d3hORzNBQ1hTRGc5WnNBRVhONUxrc054c0JmWlcyZWdtaVhQSE4rME9LbldnN3JYR0dEWSt5VHhtU3RHaTJpd1I0TmxkUG00MFRGalBsbnBHYXp5VnIxc2xOMjZFQVNDQTN4dFQ4ZFpVV0R1QlhuYjkwUlhyenA2Znc0SVdDOElDaWNhM0NaNXVveEJMV2RjTnl5eENlMUVhWmovSGlhQ2tQclZnVEZvWEpSRUJ2UTkxbVN4T0ZTSVJpeHNZMWtaRHI4OXF1d0o3VWVhMUxCMEJDSTI1RS9SbjNMeXh0QkFZVXYwTmtmSFVJRkltM1lST1ZTYkVTdVRFcFBYQy92YkRISFpLTmVqZWZXQU5qdVdxTzRZQUZzUlFyUWlpb1B5ZDNUU2tpWEJySVlzV1BZZmRvYUE1SlZ2M2ZGbldvRGdjNTJKS01GeFNDZElLYnV2STBKTFl3UGhUN0xZRmdKY1dsa09iYzJtNHZoQmFPMXlraCtXWEZRbFVERFJGSU5Rd1pINTQwU0kzSm41QmJ1UE0rS281TnhhNkpJTlRkUHpyZzJhSEJZNjFuR1hnRFFHbEpoQmphZWg2cFlzbjVPYlZFOVlEejJaNU9pbmZ4OWxpQjU5VDNyaWVucGVkbHRKZ2RjNXlyaEVmOE5oQmRXRGFVTjA4SG1PcllZclcyRHVoWnZHZjJHbkFJTkl3UXV0ZWptUU5hVDRPTW5rSmQ3Z1lUUXp4YUR6UjNWVy9YSTNDRi9pYXlOTzUrbnkvWlJJMzBPUWhFbEtoaEoxWmJ6Ulk4eVdCM0lZT3JaY1NVWE9tMGVGbUowV2hwMklkald0RWlmY3NTT0MzMHlXb2JCZ283L3I0UVVmeGhadlp5WllMdWxqc08rV0wyOHJuT1Joc1hyUkpESERsUEFCUjYzZGZiWSsrRkp3ZG9UdzJqWEFuQlBZR3ppeElBME16K3FYNG1hTUhRSHlJZ1hSNmM5QnhkMTZoU2ZzU0ZmODhiMy9ScHEwR1hFNkJ4MVhJbEZKL2FxY1NQdXY2N0JsSzc3cFFsWEYyRWxobnBNT1lWRnQxbU05RE5zRWRTR3ovaEMxWkJMS05kTmN5TDlHTzQvNWplbDV1NGRFM3ZSelhsTkNrMGg3YWNKVGtSRFNJcWxXbytWd1lQNkpyNVAyL21weUhJaWQ3K0hNR2pieVJ5eXQzUHE2NzI0OURuNTNJV1JwVVJ5WTdWcjg0VW5HQmsvbzM1bjg1OXluek9PenlQSXRJSXVDQkFGREZneStCdVBzUnVKaFJKN0dhQ2ZlSkVxc1JxaEYySnhCMnpHQVRQNUxqbkVMQ1drUk9SZVhBWVlJWlNJK1k4SWJncUZseVZDWmdtS1FEK0pFZHFDbTQrUUxpcnoyS1RrMEpXcEdaQ1FhSTU2cVljY2lPWmZhVlp3WkIvV1VkaGg4d3VwL0ZuTCtYVUxubGwrMlNYemN2ZUs5WUhCSno1bE9uTXpjSWZwYlRVeFpGSThMTUhENU5PLzdmUFJBblN6UDQ2WUN3dTA2OFY3cGxXejlKSjJZOUUyb2VTL1JGd2NYSTBPUXNLQitvVzRIbTZTSDd1MVMyb3pFNUxLRUo3aVVrdlBiZU1iWmpMVTJyb0lNNnF2RVJxNURSUWIzMDJ0dHRSeXBhMyt3NmtDNEVYckZBT2d4NVo3cWlySVkzZ0JvdDE5Um1rNEtUUURkR0JHL2lyQ1NKc3dPazdqQ2hpSHd0VUZvbTlsblU4dm9XbjFDWmswQkh0T0Y3OEZmQ3cwcWVkY3FUcVdldzN5WlhLekh0T25JbWZjQ2JPeEo4S0IrdHhXMml0MTZYd1BLR0ZhQ0xGV1F0c0dldng4cHlJaHBydVNWR3pUays4MGc1bCsyVnBwbTZ2YkFoTk9kQTBabTA3aE1GMGxCMDFHV1p4bkhpZUNSNkRPRE1qLzlVOStZVHNPREdsZE4xeGx2WWUxWFBoMTlTUktRY3Ixa1h0MTBmWE14dXlYbXpuSWkwZ1pKWG85VkcxYnhwUlc1SkZuaUVHQjRENFJBcVpVamxmKzZuajRwaDRXY0piRW4rc3poWUtKOUFlRHovS1cvMjVaR2wwRll6Q05hYWh1NE84Vlo2NE1tYldvZzNjeER0Tkw0QkpaNytXb3lSVjN4VFZhcEhGOXhZMVl4OTVFOXFHUzNXVmt5MFVZdDN4U0c3NDhhNkVaNUZBWmNES1B3RnFSNDlENFhQSm5PUmdLQjRvcGtGaVhKdWhUTEJuaCt2MHlUZWhiNDZWM3Y0U0EwL2JCZlI1UXBSUlBYQXhqNERVcjI3MGF6MkI0VTMyWEYyRnhkS1RSR2FwbWszcVU3YnVRQU90TVBNZnhCd2pjdlpuSHExbThLcGlsMjdCc0JTZW53di9UdlU5VnhpMlhNUGkzdkpwK0Zpd3FCSzlUeS84dldyVEI1cC9NTW5GWlg4WWhiYW9HS0ZNdHFNVXlpVks0d3BKc2ptZXowU1NFVW9YbWdsOWRpL09zYlZuUXZ4YXdlRUV2STBhN2poUE4vYUtxOThLREVoWktRTThmcEZkWWM3Yy9ieVJlaFAvUTBkUUhKdUpkWVNvY2gxSUdLT3RlV0xRdXZNVGhIZGhXSlhOSkMyTmE0a25hMXQxMWVFT2VEazc5ZEVBR0RGQTY5TU1vSE01a0Z5ZlpyY2RFeTh6RlFOdFZmZWg5elBPeEllQ0RQUVFYanEzSmlad0g3WEliV3I4MTZMRFE4b3pPeTNDcXNESUVwU2NTUURGazlNRzduV1k5SkxVeEpEWnZ4Z3Z2NEc3aXBTdm9uR3REWkhWZVF2SXBqYUxGRmZNcGE0VDdPQjhPbjIyY1RXRFBDUUE3S0xlRkprQjFTVVh4ekcxd1ZaWFB4QVZtWXNwZWQ2K2ZucVRLUHRtU1VueWhUWjg3WDMvRXVnS3lRb3djWWx1K1VWdWFwckh3a0hGM0RXU3h5ZjhHVDFjRWRaUHNvM0Z4ZUlxc3hXOVR6MWhIVTBRZlpFVGpiSlc0WjhmL293Um9MUC9mSGp0M1RnRXl6c2tNNzBweFhPOXRGVHArZlA5RHV5QU0yb3FGUldjWTVUOU1IUFJPU1VYd3U0V2c2VXRlSDA3NXBhYmJ1K0VLa0diT2c0NUJiK0FiTG55UFRiQzFPQ1ZjNkE0Z3RYeWVZUzc5bXY3NUFDY1pRK3I4aVlhb2piemNDWUtZWXluMlBYVmxvYVBTSk9HUVlFL21XZWFzV2RTenhKZVNRQ3lHUWF5cElQT204NEw5SzNXS1NLWForSEJFeUFVVHJwRjc5KzgwRUlXd0h2cWlNVEozalpBNUc0NjhEa2hqWDU3RFlUV3FEWndGaHk2ektCaU9Cc3JVc0YrNVlFaFBIVGhicVZpR0tSbjY3R0gyampSSDFIVXRtVHNSL2hpRmx5M2EyU0p5Tm1iUGRRRDBJK2Y2dGZrOTZiOUkrWmJLZjBkOGlEcnpIVkhKNlBOUWVsc3Z0WitnU0dveEp0VmExNkxBeE1NMlFZZ3V1L1duVzh0MENLYlNaYjBGUU1KTzVNWisvbWNPaS9jbVpUamhDOWw1QlQxMkZhd2pCaXB1QkNwUFdxdVNvcUVlVVpDVjRkLy81Yll3RE8ydzlIMVFaR0RUQjdwYmhabzRWVkNTU3dmRGVFRlV4aEt2c3kwaDM4QzFZaEpINlVMUG42WXFnZk5vTWJndXFYUG9KcXhVcmt6T1pNZlVQVjFHOXA2ZFRzVm85RVlIaTI0ZUR4NTV6MXhWb3p6S2t1MXBaMDA4ZWRnVERUUFpsKys1MEQ0MnhVWXFwOE1RRnhrRThNNUx4d24wZ2VKTTV5ajZPQmZUK2RmOGtoY0NTbDBIWWZkUjhTbzRSVW5JakN4V3FwOWx1azRMZFlneXFLZnp6QWFVVForcEx2T0dVa2M0VG9zMjlPSW1rbWxhUlVvamxPUnJESkRiWFZMS1ZxOFJRcEx5alNRVW9oWEh2U1pnT1FQVnFqNDUzdW9NOGtQRmRJd0dxNVhGeC9lYkxpeU1RR0xwMkc5YnU5T0YyWWRvM0Nxdk9WbXhkanlxVlRpaXp0eGZyM0pKNjhYTS91eVZDUFJOVnlGbWJtSHNrWUZCblVSTEZjUnNiN1RDQmQrOHJRYXg1M08yclI5MU1MeitpSjBMamUxNG9qVDdhaThPRnJjL2g3bEJrV3h1ajZVaEtsb2NEcSs5amkwVXlFU2tBYjJvaTJ4MkROYVNvRkQzR2JMcjFmMkk1dHRrMEJpSGNFWXE3eGRBaWl3VTNqYndtT1BBdlBHZEw5c1BCaERydDlDTUVQTzc0Y2pyQjgxSlJsKzNOVUd3UHdPU0Jvd0F1b2ZjUlJmNXNwWUtnZ1ppRFRVeWFjZURpT1UxRHhSM0FmNVBhNGhOaE5uZ2dLV3RldThmSHQwTHdqeWNKcXFWVzRZSzc1Z3NFaVg5UmNnYzlQbnVyLzc2K1ppWWYvN1ZjZy9tY3QxTXByaUtyVzIxTEpSa2JXRFlFeVNDQTNOamJWZExjQlRIWHA5eG0vZmhJK2dmSE9hbmpudGUwaHZST01IWkZlN2hrSExKQlNTUU4vR3M5Yy9wKzdtVTkreTdjZ2pNTVc5MXNPWS80MENmWWp1a3RBMzNUYytmZGV6RVZOS3p1YzgzbUtvZCs2VkNCejg3cEREWnFEWG90MldPYUF6MlRtZGhndXRqMzBidXhnQlVrMzZyV0ZQemxXbEREbjdwQTRjRFU4aDQrVXZwc0gwcG1KUTZSN0NTNnhGRnM2R1VRMmZlT3JIL1VIQ1dWc0cyZkJmZWJZQlV1c3hHT0Nvd3dQM0xyaFdsaUJ1c3RIV0o0MkNaeklYTzVYTFNjbjZnWVBURVdvR2RJNWgzWmp0ZmxIODNENi9LV2cyaDl1eitNWkhRUXJiU2F1czFWbkQ2ZWxnNG0wU0xycEt6SDlNVlNVWXhBTXBhUE1jU0VZVSthWTNiblU1Q3FHbzMrZU5qTkZaWm91WFJ5aXREcGowNllnempvYi9Sd3RTN3JMMkU0L1pPeTVVUGlaaXlZRjFpRnVrODZ5RUJLMlNKS3VhU2luNVMyU2VkUFhOUDY4ZHQ4QjdyYWd5S3QyTmkwZG5pZkdMbDEwenRDWHRsN0I4ZlJ6M2RoM3JVNEF2QWdsbmg5alpvNFdxM1dXRWdLR0dmcnRuWDVxMGIzSG00ODRtUk1NTmI3ZmRTUWNaY2RmU0tsT292L09oUVNyai9GOFR5S2VndDFsTDVveVZheWZNZHVkOE9FTHIrMW1iWVZWb3BSUVJneGFqcTZXTjEwcjRmSUYxZjBDbEdzVDVyYVc3Z01BdTlFNmk1V2NIRmZYZURKeEZmSC9OSzVMUEJBK1J0M2sva0dDb0hLK1dnUmRkQ2hpTFk0RGszVTA1bHFBOTlYcjN2V3NLdjN3cG8xL3B4b0JvV2g5SktFYUVvMGFpUFd3cHFtVWpTZDVXajFDZmJ3blBxMzBHZThGQlhUSmZqMHo2RGhhcGNhVGoxTEVkVGZKZmQ5aldweUhjUDJvL1ZPUlNJakdXano2ZHJRMEFwWDJVWGc4dkhqcUx2WG11QkI1ODMzWjJGK0h1T1F6NmNvcnpuRWNEWTh6c1duZGNjS2szaFR3RlY5aG1kWmZacHJ3ZEtxVERleU9raW9mSXozejMzM1h2aWEvZVdZSFQzcjl3ZHdzOCs1ZkZnci9Md3JHRERsZmIzVmhraUR2TkZaVGdFLzZWenFFVHV6WmRNVUxIQXFhYzRaT3dpQzF0UVp3Slc3emJvYVdGMTBsLzNNcG5WNTFyQ1AyMTVkUnlUdzJVR0p4YVVSbXd1cHZoMWxSWitnMVJGWlgxN2dpVWFsdHhzQnBLT3FGSFRXRFBGQTNnQkZFSWc2cGErZWRkckJwMWs2WW13dkVVNTNTVVd6d0x4NFZkeGJFV0Q2SHQyMm5HdTF5ZWIvdFNYdjdjMGhrMmg4Q3BQUDgvelhtbEtVR25OUTRHSW84WDFPTUF5NEl6VXVnRVl6bVFOcmlsTmlPa1dLdGcwNmcwQVNid0cxVFgxTnA2WUYwWjBUTXBlS0JIUk83RjJqb1JVbkFnVmtmMy83K0E0Y0RVQjFnWkRuM2NCRGwxVTVDR1NMQUFoOWErZEZrRlVOaU9HVUU1MklYRmlBWmJsRU1YU3JMblAxT0hOMVZVZjNiQTQ2N2RnZC9mSWF2Rmp1SGFLYTZKNk12NlNzRG1sM0orR01PeDJEOTVEUldIM2ZtQisxWmptbTZhR2xoMzFRZlk3QjlLdGZsanRLd0NLU1V2MVBqaXRyZDdRTDU3M2phNGgwWnRoMGRIRWxIcllxTWhhWDg5WWE4aGUwbzRSOTVXaTVXU0dwakNaWThFbnBsVjl5Zkh3RXFmRElJODhveW9OYzlTVkJvM04xaVFRalplZFJXUmpMbk9uWU1JYjZERzRENTVDa09IK3hFMGY2ZXJFMnY2eGZsQ3JtVndRTS9vWkFiMHNGNnNCNjFpKzdncnVxZk9KYXQwMk5BbDA2c1JsSzBrNlJLeUJKOXZmdVFyNXFoTjhLZU8xSmdmVXJSekxsTUVlcFZVWXZKMEM1MDNDSG91d0NGOEc2ZUZIR1BNVWw5TGFIQ0FrNXdCa211QUVrdnZ0d0dxbUh4amZ3MzAwYkF2MGhCSWpQVkk4ZWpEajJVTkdqV3ltN3RDaERJU1ZzU2Q2Q3EzWGxJdEVRQ3didTgvUGdrckJvQWdGTzJHNm42RWxGRWFHY1pnN3RXUndzNFZUekd3Ym1FeWUyK2dkTnZxbk53VHlJQlg4N01zZXhvVUlZZStOSWZyemxsQm9DUUxvVjhqR3RDelZYbUY1M0hoSVJmOGlseXY2QTFtSTdhSnpCTUplT1lud3pxaU1aM0ptTHlVN2dJYXB0bjRTRjNCUGJtbVZJVTZwa1NteGJxOTJNZ0dNOE53RmdMTXJ6eHFlamlZdWhnZkxDWVNJY0hCMW5ZSGRXK0crUUhSS2FVTXViMVg4S3c0d2NlRjFod0FYczBSbzhsdEtHYVFxVFhmU2J4d2tQZi9MeE94ZDhEYzJ0YjVoSXJKVTVaNU1rd0M1Ym1ZWHBrRkFpWjY1R05CaEtudldTMXRITUUyZHZGS0dNaUlRTjFVd1VhMDdTNmQ5ZEtyanR4SU92aWh6UUJDcUk1Y2Q4dTZtV0tUdnUwbFM4SHV2Q3BDMTdTc2FTVkZLeU5tNE9Qakp0TjdydzIxcEdQUXpNTE1zL29kWTZNWWNoQXFFNC85dDBNUjdubmFuNkNJSTNIcFZSa2d5N1RGZ0dFVXhBbUVyVCtwNHJPWUt3aHBPaGY0WFpMekwvNUpyTlY4Z1VlTVJKODRLUnZPaWhFeTFOUDBVdlYrNWhlY1pDKzJYKzVSTnBPRkxLRU9TaVRlUExvbzV2SnppaDVwcVhnYWhGZUpQSXFmZmJERUN3TVhmNVY3a0Z5dUdFYUtybFBMbWdETXRlTVJBL0Zjcm9kUWI5bGgzNzJVY0VrNTZnU3VxN2tkMVVyMUJ6WkZMWXJ3THMya2JnQjIvSzVNMmdXWi9IZ29ZTEJxcXRZcWhjWDRFZnlJT2kycjBnQ2Y3ZjdYZG1vMlBDTjZQbGpjYTRpRGs3QjdvMmpGODl6NXVRUFBmTFRmWnlqUEdpbkZoZFZhYTZ5M3lTcFExelc0RVhnT1hpRXZSSEZjQXFXZ0FCQktQTzZFTkNqNVV2OTlldVNNZ0Q5R24rU0Jta3ozUzVyREZFaUxKQ3pWVXMvcm14WTFKN0hzeXkrWjVXeWp0TXNtd0J4S3JzR1BMMzAwRU82blBHUWlCVEFyR3RwektlL3k1a0tiMCtQY1hqdGwvTENEeHZCcW5tTUFJM2RjTGQ0NzBOZ1RKQW1ueEJXbFNxQnZYZjREMjBaNmord1BWcXRSKzhRYkNRMGljUWZHeCttdnJmaUhnWnlodlJjVm1DZGswYmx3RkcrL2RYVVBNSUlUMHV6MVkyOTd3cmNuaE1TZUpRbVh0SVk4QmZhYm5hc1BWaXNVRThQR2JwN0pBazVsMDMvT2M0MDdLdm10Y3Q3NUwvZW9XQ254UTBoYTBnSnpUZjFudmI3eVRMZ2dSOExqYWhLZGNYMXc2RXNJZGFrMytzeVg4WnJDTHhCNCttY2VKQzZYSDEwdkN1dVlvKzRmbm1WMWgzb05vempKN0o4N2l0aHZySjlscHYrVGdsWlpoWHJZaTRDRWZ5WXRCRFlBMisrMldUTHdoNmpvOW55dTJodWhiTDF4SGVPalBpWkg4RTUvVC9NVUhOU2UrZ2tnMGdIVFZ6YWVkOFhmVUFoUkptbzRGbGhXQTIwMWl5UHJSV0I1aXF0UFAwL0ZlaGVLYWdNWWpVY1lkL3FEOU0vaW1FaGNTTHNTSHJ0SXFLLzhzdzBiNFptRzhCcTNCTHlCZ3g1dWVQbFRDWEx5dGQ0bk9vUXBxbTluYkxibmpySURVc3ZaUXJaT0NXK0lhS2RsL25FZVdNbnBWVGEwY2tNTWF0NEpmdWdvSzJmKy9nYjFqdlV0MzZLR0hIR1B2VG14a3JUT3NtZGZmSGdVbWxEdHVxbWViL0NPWjc1UkZxVVN4T1N4NzYwdkZvWU8xaGRLMEpwS3lvN0p6Q0ZQSEZ5eXhmck9mbXMrTHdTdndXOVhycUtNRUpQWTA3L2NwR0lCVThQQTZXaDVla2k1QXpZZDNNUWhyVW9PWWs1UHZrLzJXa1BJdVBxU3loejBxRklCZnI3ekY1SkRpbnNtMnVCaFZaN1Rzd2QrUGFMektlRmpPMkt3bUIrV3kyUGlZMVJPR24vbkgvSVZ0aHNMeGZQTTZHTDlEWTBNNGEvUDBYR0Q3eEJYR3NqWmNzMUY3ZjRIRHJqMEtzcEN0OGkxdEF6VlY0N0Z5MXRTSTdGUlVZRC9lczE4UDlMdk5XUC9rUzlGaEx6eFl3YzJnV0IwNnBCaEFUYWdGWHk5Z2Nrazd3VzNybTJQV3IrM2d1MmtjR1h4UWxHSkp3cTJFTi9xSmJzSGpHTnRxQW5CdnZoQUhxUnVHNFc2cVJiU3cyMVp3d2k2VndjV3VqNWtPa2dmWUxTRkRHNWszbThSUDVLdlRJdDcrdWdJQzNRa3ZLR0hUTEJmS01kMnBiZEJOQUNkTEpZSVplS2dTa0wzMktPaTMvZFBrTlNRTzQvYS9Hb0VydWtGcWt5blVJOEh2K0dTK0wwMUxaUHRHdjIvZEhLYjJHRW5WT1JneTI5NHBQS3QyRnJ4eTlnMnBFRGtGVzRNMUNVZXgwcnNkWjhvTGZNR1QxTmJTc1Z5SEVWaExvK3ZMSjhtT0IxQllWQmhYL2JLTjAyU2RURlRVS0ttME1qNSt3RFdSVlVqV2JVTzlzRFVoSVhER2JaT2tzMTNHd0lpRFp3T0pVemk3Y0J2ejBvNGluZE95RHlqNUtaR1BIZndXYU9BSStKWU1ZN3NjRjR4SDZSM25Vay93VjZFdTFmdWsvL0oxWC9jMTNLYXR5NUpCdGlwSmVEaVdoRVBxY0Y2RVgwNk92Qmd4dG1BLzRkVWFaMDhtaFJ1Q2F2M2FOdytCV3hIR2piOTlsUVUxWVZQYUNsYmNQTExuZVYrL25GSVlXYWd6TmsyVjdJNnNzSU1SRjJPaGhqZGlaeWpUeDRwdmhQVHdmM2JWVktlWk5sSnBZakZPaDFDTlVKRVlaa1gvL3Q0bmdYd3IrbjBCYkZyQ3FPMGJXT2NtOW1GQjVLNy9ZU2VEaGhNb08yeEthR0l4NXhyUXRSUkhodXdLL2szV2V5dU8raE56S0toK2RHOXZsdjFnQUpwdW5yVTBlckJMTEIwTWJKSlp0Q3FjckJUM2JoYW45djRieDFmOEhnSEpwSHE4aS92Qjg1dWQwKzNza2hGQjVFbGhMVVoyd0pXSm9WQ0ZYM0lQdUJMa1MyWFFMTlJaZGF0U1ozU3o0bGwvb1VRVjkramNsVHZUMWlxY0ZiRUZnUjR1RWVPblM3N2RTWTlwanJvK0xRWGtXbisrNUNDS09Ea2lvSHpNM25lNHFpVkhwUGJkNDM5L2pxcUpjSEVIRXJ4Yk9YUm9nb0JHYThzUVg4TTZlcDZWdFlSTjBnUVRzSmVpSllGU3dyQ04rekhuem0yeUFoVVRGTE5hWCtGbFgrbjVmUnd0MEhMWkNEb0c2d1FzUklSWFdCbU9kOXBOUmNsTWFQWi9RMGhsVjBSWk9GMUlYWEYzbS9CU1pFNTJFNzR1aUNBdUZQQldKdW96NmRxbEYwSmtETlA1b3ppVEtXL0FlUkM0ckhrekFjU1JwRFBiTlhLVnpMMmFCN1ZkSURsL3I3V25NSmU2NHNvV1NmVndIcUtpWHF6WGdWK1c4ekU2VEg2aHFIeWU4dllhM3NNWGExZmNKcVd3c3RNcGlxR2MzYnZGNUM3VE1CUXpjR21ML1o5b0pWbkl2NDNrN2w4ampNZWYxZ1dMdFVqNmx6RjVNdnpXV2MrM3puWXExR2I0RXJxQ0tpdmFrd1FOQ1hXeUZ3VEZRQkdRczk0NnVJOHNlUkxxRVQ1UWoyNjlDMVFkVGhwdjY0clMrTjhtQmVwK1RIL0p3c2h3R29sUFYyU2w1MlFjN0NVQVVvOXBGcEV3SW5WbDhsdldGbldvc0pjVGhIVWVrWi9ZQXlKdGwzTWNrWFJPRzhhVDVVYlBBTHREK282V0QzaC9aZHYyWjRDTkN4VVdEYVorM2NpZVIyU2V1a3lRQzFKUjBuY1RYMmRkckttUDBpSDJ3QlhGaVUyL2psR3NpYTNTR1NHQllmenFRS2JYVVRLOTI5enlRekU3UzU1ZFZLUXlIei9KN081VWVNUXJIRmh4TFBVRkdiWDZYSnk1eWhDRzdKLzc1Ylp2Mk5TY2NrMTN5YmdubG1XWTJMTldVZUVZVjhhK1FreGNpMmxmWmVPYlhwcTVEanZOajVwTlNWTXZpZ3dFMnFDQlZzdUZaT3A1OVI0cUJzTTVaL2xFUTE0c1JuWVREUnZ1Sm5aQlBDd2NqaWh1Tnh3d01QZXMyV0pOeFFKOW5MbXg3VjRQeFpmNkMvSnc1amp0VjZhUUtERXJtMFdPTXlDTS9zYWNCQno1Yi9QU2RQWDdZTUlSTXhjeFNaalJVMEp5QVhDRGpkQlNuVGpuSG1vdnRLLzZsQUJJRlRnNEtJelo4UW8wYmdlUjMraG9yWUFTYUxOcDhWbyt6YkhZbjRIaE9SREFJY0l3Z285Smt6YVlPQk4xZXVuVWN2NThsUkg4UHZyNGY1R0lNdE5oSjdLNjRDRFppd0ovUnFmL3RXeWN4Y3lBVkdsNURjRUo1OTc0N01zeDFhYVpBWFRPOTgxNlFxdUNpM0x3UlBjV2RjanlIN1pmdnMwTld1V2xXTG1iQTRYeUt6KzJXUzVWdVN5bHEzbkdIZGJUOGVSaHRtQmp6NEZKT1h5RGlCWFFKUWc5NUVqYjlXeVcrbzU4YUluTXNkOUI4eWMvazQwcHNNLzdFWTd6dEIxZ2dmbG9UZU1JUkcyd3ZLdTJSc2FJNkdNOWQrYUNOMDcvTXd3S0R1aVpOZDdxbHRVWWJWRFl4S2c0NEt4NG9TT3E5YjJrUkFDZ3EvSkVCdjJvMHZxQU15L0kzbXVnb24zSjZYVUFDb1p0WW5pZ1RuNFJTMGNtZDRTQ3Jld3NtWW8zbkNRdXc5VHQ3YkVpVTI3WkJQNkg5bGx4QVNNVVdnRUZZZFRub201V21aTE9vaEl4OFBBMm9lTC8ydFZUSzlXaStDNmg0aUlzWjNlSzQ1S1VYck9FZnRSZzVscFBoUjVCOW9qclNCZSs1VE9kamFhMEhSaTJDQlRYWjJ2TlZRZ1NLTjdFb1dQOHZzWXNiTllQYUVYM3BmcmY3RWcyaXM0OEtYWlNxdm4xeG9Vb3A5OGFicU85S0pPM2V2VjFGdXJ2N0tEdjZtNEhMMWprYmNmcTVqcnVVOTZpK0VIbUc2dTNJcDgzU1FpRW5zV2pLRkpqU3kreTFDbHFMSDFOeWRvT2JNbEhsUVYwSVRmVU4wVHp4eDg5N1VnM2hpeEwrUWd0c2t2WDZ4SHpQY2svbW9MNm5VSkcvMy9EczdySjRub1U3bDNsUkROVFZrQjdXeTJ3MjVySEdxTUNGWklBNUNSM3hLNHVFc0t2eXdFVUdSa0NlUlVFQkZuZnpmalRuUk1RQmRDSURWMDFhV0ZjMkdhdThwbzN4dytjcDBQSnd2eFlOeFBlYmhpM29neml1ZVJJODlheXkxWFFaanJzVjliYkp3a20yL0ptc1BVMENnNHhFYTJlS3VOMDRsL1FFOHdzNlA1cFVHK2tpVXQrL0pPL0pjK2V1ZUZRUFM4ZWVlVUsxanZrMDQ2czIyeDlSd05lUjNkOXRLTzU3VzMxTWFWKzQ4aE9WSG5QQVlwcThuenY4L2VWb2ZxajFTeHBPRXRRcDdCSWFHTlhYRk4xUDdvblZZQ3BqTllVL0hMN1lHM055UFFqU0VldTI0OTFRNXR0UHN0Y1dJbFVCUlpPUW5FOHFkdjIzZXEwS3JjbU9wZXRFSmVleU9ZRFp5N0pXRDlGbm1jRzdweHFpbklJbFJWWkhHTlIyNGhZZTdPeTZQaFpLWVNNNlRpRDJxQTlhSnBlUkpUQ2RtZGRDMFNEemR1bGhFYkhwNUtIN1RTNTkxakpHejZwbi9aM3Fab0JEOHlzNzJzZ1k1U1lpVGtLMHdHYkE2OG4rdHpZTHc4bGEvN2U2RjMwOUhOdmQrNFZFdEsyQlFQSmxpRUdNV1ZjcHQ5U3BzT0FDRHUyYzJPQjZvY0p0WS9aNG1HNForelhzSjQyQVRBTkhQNmIweGtxWTBsOWhFd3NWeDZJMG9GWUIxVXVLdjltMjZtUVFPRDhxSFhoMTVXdk5Fd3ZSRzcyV21ENXRQenJHZzQ1ejgvZHZvWTIzV0QzVUIrZG4zU1FvaEN1T212QVRxUHZ5Q2hQb054TmkwQS9mRWpDV1RJUXltQ0RuUEtVR0pGMHljSkhaK2FRQ2YrY2tKNW9hMmd2Qlk1Q0hTWnB6UE8zMzRDck85YUVMaEhyTTBNdG1MT3g4Q2lRTzM3WFdUdTQ0Umwyc3BuSmJYOC9LcXl1VGdaazBlZzF0OFFLcTE1elRRR1pTMGFKa05LQjcwaE4yTEE3VDM0TEw1QU5pOW5BZzlnVFc5cC9mYzQyQTFvWnlGbWdVcEcvZUQxOEVpU1U0OThiNEh5b005UGN4bmxwcXhKRGU2dVlKVlI1TU01bE5Ga3FFenpkS0V0RjhXczJqYjIyMTlZWmdWT3JWMkxzYVJkZ2U5V2JrVUxUSzEvSHdyRkNzTWlOM0VKd0dJL0FIRVo5bzVjditaUE93cUV2TXpVcGF3K3g5NlorVXZuOGx4NG5PdzNjNVBmbjlnVGxNcGZHRnArZVY1eUVzRHl2TkhZdDJpVDdWbXF2bDB4MWNsWkR0OEZ3WW1UYWhvUEtKN0QzSzJEQ0xCRTJaTlpyZFovS3h4Z1UzZlU3cWlEMld3ODJ3QmZhVWMzS0xwQnhqamR3TzhDZ2FPNThORzhJL3hQYUZyRENkSm1PdjZ4RENCRXZZNGpuRS83SHBtVDBQcjkra3VjZ1dKaGo3OXFTM2VTdFRFUHJ1amFWRk41TlNJWFJobkRkb3poc1pwYXZhcnpRQTBSYTZNaW1FQUhGUjVPcXdYVG9Sb0hTY0F0UEpIemJMbVN4NHdlMHBxdVV4YmJoQXh5VXJFa1hscE00b3BDd21jVTRwRnFzaXVFMkJWcUd6U29mcWdoRGl3blo5L3dtbDZCOGU1S2Z2bHAvRjhSZDZvcTJ5dnY4Q1plVWJJa1hDTUxsTnV2UGh0RkZSSUFoSGdBbkx3NkJ4R3FRY0VpdzlKYnErY0dSeExSR0tlVzZldXVBamg3T240NVVheGVQeGxtK0o0MHh2MUtuOE5XUjZhT2l6elN2ejc3d0VuMlRndnYva1MzOFdDcVpLOHNpRUF5VkJCWTRWZlUwOXVYWEJ0aG9nZ2sxRFc5aVJKN2p5U1RjLzZFbm5TcU9QMTROSVBmWHdaMVRBZExzZERnTzAyeDE4MXd5RFJpRUZZVERsYzZEYU8vWE1TRks2SWlGYnVJcEIyN1hBWFcxUVcwNVVnbUZLb1lTY0lybnpNLyt3YnRQYkJyakhncCt5RWRjbW8wTFo3ekw1VjlNYXpzQzNtR2dTdkExMG51QjdqZko3RUZyYnlaQ1A3cFAzeHh6TGxaeGVZZml1WFkxRCtoQ0lXSnZKSisyeE5PK0lnai9QUjI4dXJ4dU9RWEdSZ24yV0tuMUpGRTh0VlhUZFlkOXM0ME95OTA5R0x4Z0wwU0hiemVNY0ZNWEZ2VXYxYXhkaFM3QUJiY0d6TEpZaEswUGRudFZVT2RhMGhVV1ZScGR4bHhXOWp0amR0TkI2SFdxMDdieFlkOFZhcHpQL2VMV0tQUHlvNVprMzQ1ZTFDVHAwNnQrQWtFdDVKeDhNOEJMTldxNGJnTUVsKzhYZHJaRjdvZWZMbzBRVEppVDhXckVCY29qSlJab1U2c29BZStUWkNGOFZLanNQRW9MZlN4M1RFNlpVK2pJVnZYQXJQbDlKT3ZxUHFPQkdOWitUNGxqK05UN3F1N1didHVwcTBOTzhYaHNBUENmbzRoNWkxSitkU1JxeVdZaGcwNnE4OExyQ1V6UG8rbEd3RGoxN3RZK1g0aU50WTRSOUtzYzFMWG1MR0xiQlN0eUgrQTJQeTR3VVZHZ0ZmaEI1Qzcyc2dzZjF6dmpYZzMzK0lSSGhTeDExRDJING1Edjh3MW9IVHRpY1J6THdEWFZWNFBtb2s1a2doV0dxVUIxaUoyYUlhWFhndHhTSEN4b0xCYW5WbGlpbTVENENzYTYrK0VXZy9YVzhWRjBsTlQwRmFHb1NVSlRyRmFnVjJINGdRM3hhZnY5TmRFWWtlbDFrRE5YOGxqV2VuL2t1TFpFY2xZR0k1dkY1RWdFY1VZdXh5Y3lyZ1RPNUdtV1VpM3g5QzgzL01YZjNIRkQ5V0xWbWZRQTU1K0pyaUkvVGMxUklOU2hEenpYRVFCdDZiUjNtMTdPeVRnU29KRkYxWHNKY3g4WmVFR0pFUXNWZExlYXVMNUdGOUREdlR0RnRZYXZjR3JWTm1TY1VGb0ovNWRFS0JmelVUOS9DU3gxalFPRERSMFljN2ZYYlQ0WmgzY2RUK3MyeGk0QlpqUXNVQTJFekgrTHVyeHVjZ1g3ejJmVkxjakMzekl5ZXVmK1VUeHJ4Z0ZpT2RiZTlqejBiOFBWb1hUaDZwQzZwMlhhaUV0UmxQY0JDOEVKWjVFaGxZUnFrVmM3VHVONkROcGl5U2tOSGRxcE94ZFBJaG8xcTk0YUJVVkFqbjIxdExHNXhwWC82a0lnV1E2SHdQSFBJbC8ySkZZRnU3YlpRNWVYSXc4VGhzZzlVNU1LM2U4SENBK21qN1RFZy96a1BGUGgrRTJlTkFFdmVocm9ORUJjaEF1NG5NRVFWMFViSk5VdXZwdXpqZmxBTmhrc3U2OUV2Y1hvWVlBWGdLc0w0amZCSjZBazlpK0NvN0NRc2paRVphRE1lcGM2ck1vSStCMTdlQXhiZ0ZPeWR4UFlDRHkyWTZTQWhjT0xjNWFjRzZzSitQc2o5UUlROXhvSUNOcVVLUWsyT05YZnF4OTFqM0VNMU1wRDdNZE5hSDlaQjBwd1lHU09pbkdNZmx6ME1DUG9HVTlMREpzRGxuaVIyTm5lL0ZKTVNranBSYUhsSkNrckVsMDZ5NGNoS0FyTytCVmZ4SkJZczQ4SzBNMWgrVmo4U1BEVFF1NUZIeFhEZ1grdFhqNW1YcVlCeU1mekRIWlRGbXVLRG42MzZiNHYvd0dPYkpvb2tNck1PUzliOHdsWHc5am51bWVuUGoveWw3NDdQb1dwdjl3Z0N1MXR5L290R0hORGVXWHVwZEtVeXQvOWZWY3VOeVBESTRhcG5ZUGJxaUx2RGFZR3NjSjNYVFdPZ0pLZ1pHOWNLenlkNERWdFZPN1h1OGdMb2g3eklJVHFtRTlPcWl4amhKdG9TNlE5eE92NitOWmd2UG1EWTRpNkRQdVpjU2NQTm9mUy9VSktmbEJobG84bXdGYUlWeFp3WmN2QmM3QTFPNDJGV3hvR09lbU1nNFRUTXR2UmxHNzJVSFh2N211ajdLeUV2TEpDWXdvRGZFNEprazBqNjBUTk5qTEJ4Y241cGRiYlZVOHV0MXVpY1QxTTNPNzI4YWFtcnF0S05pQlo0cXlKN3Q0R3ZmUmhCcHNBK0l6WjNRNWR6dnhLUENpL0RyU0kydmtscE5sNWYwV2RQUkZUM0k1bE9ZL0ZoVEVwVVNVRFd4Z2tRWWw4cmhIb2w1UXI1ZUluSWhZV1hURUxXYjFUb1E2ZnFxQUhacUVOazY1amFKc2l0Y2lYK0FJbnJYM2tLclp1SHNpM0RFQTMrZTluN25VN1grNTJHZ2s4VmgrVmM2VERUOUhacVh5Y0RlTU5VY1QvV2VDYkpqU0lyRWpkaUJFMTEzZ3ZLTE9UZ2h1L2J2Q2xZaXFSL1NyS3daNWpTc2lyY3FlemZjTFlTRk5GV3lYU0N5STBiNlpJL3hlbjhCR245dkpoYTdUWkFQUDR5b2tOMndtb3FVT3p6N1V4TUh1bFBHSGlOUDVRdDRYM0RCTHdzVzZUeHRBWS9JbXhRQXB5aEg5RWVIMTVuNDduQmNCcWVsaDZQVTFORGxCNVRGNkVzcElaekdEQmgzVyt5Vk0waTRIbG00NmdJZFJWKzQyTHV1alNOUS80N3p4K0dUT3R6bVVsVW9jNVJwaXd1ZjJnUTlwV0JJMjJWSE9lZlFhK2IvcElpVkcvSTVQYk9TcWFybDRoY1JLaTB1c2hBdDJuS3BBNldGL1ZYam1Yb3gvaUhPb0hhUFBsWGM2bjFnNGk4WjFLa3cxQUowbkp0LzFHdVVhdlAwV3RtS1hNYUgrMFB1T1dLb1IxRHRMSlRrWEpkeDNvMXEyQjRiT1FBdHFteU90aWZ4dzZveFovSHJnQnBUVzNjSUFJV0xJOVpTQzVwcEl4S1FrZlF3dFE4VXliNk1WMUpqeUFxakowMzh6NGY4cVRDb3YzYVl0VDdhMWp1MGNtQlcxOEVhWGdESlh1am9weWh3NXgyZWNXRXlFZDZBK2NocTh6K2ZWYzVxOWlXTkFnUnVrTkp1b2xJb0NEb09FUjEyVVQ5aXdZZ0JVbk9Eb2M4UEl6Nkh2Tjd1Ri9OeURBZWRrTjZYc0RiRlRjVERMK05kMFpoNEMrQWRIME5XZjN1MTFxNEo3TkZYbm1CRG8vUUtmN0hFTzdzZmE2d1ZudnZRZzdISWV6ay95WWNncEliYlY4MitYTXNhcUIzeTBUWWppMUxXK0xYczBLdXpoSEozdFdXY0p0VS9SZmZwOU1uMHhyZTRDSGVUVlBXbnNRK1R4S2Y4ZGswclRnUkR4UFJkNW56QlBoeW94SmtmYlp4ZldHZUJYeFNDK0dRM1VGUUhMRmtwREg1YTU4V2F2eEV0ZVQ4TEZ2emNWV1hGTnhvU3N5U0hadGZnaDlMQ2p1OCswMW52MHRhRkRRTjczYkxJMEdoNUVLenEvRm9rQUM0Nk9Fd3JZTSs5QlVTQjVaV3hneUo1OGRrbXJrZmYzNmtpSTYzRHN0MGRkKzV0aC96Z3Q0Vk82OGFhbDhocE5vMzhvVnFNS29wRU1Nd3I1bDd1WmxxTHcwNkhZSS9HR25vU2p4NnIvNFAxbGs5ajhBYmxpWDY2UU5hL01MWklNM1FUREM2VlQ1TG5heU1DWnZNS2RYVWVpbGgrb0tWZGpOOGRNSlRwc2RRdk5JSUpnYjNBK0VETFBjUVlmaUptSzJrVmFpU2VWbzM0Ty9MYUFoUnQ5MTB0THh6MUN0Unc3SEg5dzAxVm40bFI0MVBVM3Q2RHoxamxJWEtUeWlPZCt0amJ3UWZrZnp0N0NkY0dIWitydEZKVDlZNEQyZy84c2JTck05VmMyY0FTMFZNeHN6RzNoK212cmFBVU1xdEl2MUQ1dW5pTndBRGNYV2d6bGd2VkJyR25qVXU5QzY5am1oc3JJbEFDQmc1NllUb24vcFRNR1l1aVY5NFVIRUhOZEp1NnpMMXM2WDV6SVhmbEZteDVQN1d4VlhaLzNEajNIWWlXRHBwVWN0ckJ5KzF6SndEK0Q3aTdzR2dvTm4yTExycGhKNnNUTitINzQwVnVQS2t4dGJjaEl3QnV4SjIyTVNqYUszd2d5dGcyWEgvTXZPbXFRb3J5TXdSdjNmemVBZVVhaE9pckFIMDRzSVF6bC8xWnNJcWdmWEpmUWdod3FYMDZHOVJhQTJqaWZhcnlLQVF1cmRNUkFUZmJwL0xkRUJhTnA5Ry9FdmwxeHBxOTJlNGVTamtma1ZvdzhDTFBFZytEUWs0Vklkd0c3UU03MTRETjRHT215aUdvTm9PRlhKWHhMZ2owYTJDMThXeXI5eW5SUzQ4U2dFMDBMaXlwajZKQ2dYb3VFMFhIL0p5Nng5R0tYMC82Ympmd0xqVnNCcmhwaWhtbkVlWmc1U3FzWFMxcjFEN1FrMy9zVXU4R1lKNzFMb1ZGaHRRZFMzMWU3dXZac0NkVnhLUXNJdFlqandLUyt4RzNoejNnWll5SlFETlFSaEp1QVVjbEVnd3BhelR4dWVJY0xLRi9OTlRXR2ltSURQL1FxS1JCSkx6YVJ0UUtKamxna2xGSG1tc1ZUcFVzaWV1bU1jOEFGV25TNk5OZFJaclY2M2NNdFBud0tLM0pHODJ4SVh6ZUZBa3hZTGNSbFRNZmpXQUNSUkhZcFBqb2Noc0lwWnhPU0x4OG1xdlNYbUpRNlA5bVkxUURyZDVQclJESEpkNk4zTFZIaTZKRFg4bGsrVzJ1QVVsTEx1TmdtZDhmdTZ5SkNIbTFxcUZBNmxTajhrb0dCVXZjQi9GY05DdU9ZaTN6bFJuU1Y1M2ZPRm9UbnFucXdEY3BsRVEremNCVVhtMFFsbDRFREFQUkJuUWxibUxyV2I2UU84R0FqV3BTMWYreDViMlR1amdwUWtGbzVUUDc3NVhMSkR0RmgzWnZOWE9VclRXZG5XYWJqWlhTUXVhdmNJTWhVK3ZCai9LMFdEaHJaYmpwVUpTUXVlSlR5eGZyUWRZeExDcXBHL3c2dUNNWHdBWGNKaXFBeFI2OUxsNjMyWTNOaTl1UGRGT1JNUFNsMVRYNFVMYm1Icmd6TzMvSS9ma1NRWlRDaXNoNFIydVFzc1hYdWpLVmF5WTVxN1VzaUhWWjViU3czY3BuWXFaSThwcDhXZzdWSnBxU25Lay9QeVp6QWRiWFlEZk8wSEFVVnkvUnRXcTE3S3ZUOHZNdGlsUmtvZTBic2MrNHBkZTdHNUg1UE9YbWo2UDdMM2lNdG43bFFPWjV1WndIL0pGbVdvWE83NnBQVTJWMnpKQmQ2VkFodWhtOFVGeGpKam95NXlVaWxtVnJza2t1VDZTWTFkVGw4K0xINVFPNEYyRE1SVzJkbTJXUVNQVThQeTk0R2JoYmlvUzU2UThNWkFGa0MrWGtVc3loODBkNnVySEF5a1drbllqd3VIY3YyUGpLUEw2T2w2aU4yZWppNGtZdHZjOGVhdElDZkdvT1RzUDh6WFE5dEZ2VUxNYlBBV2NXN21lV1g2SjExRHAxQ3ZrOHFsRDNydU5yaWQ4M0RZTzNTNUg4ZnJrdkRicHd5ZmRJTzltWDVxdG1EQzBGREZXKzNndlB2ZDd0T1hEWEx6SXJqY2srQnIxR21PL2tZTGNoa1QzSUpDd2Y3VzFsMTdwOExlWTAwS2JySEZLV0h4WU9SVWU0eFpSZGFqdGlhRkt3MzJqcG1iVWk1aDVqaTRoNmdGM1NiUTBKeE5QRWxJVWdGVnBESDRac3ZGUkpIeittMFNWVTJteDl0YytobFkyaHFiL1FPTTd0OWxKQWtlM2s4V3NtZ2kzcW93WUkzUUhEN2owYmtseDFYUFlaZU9xSkhlWlNZSk1YR3R3TkJNTFIzYktFSzcrZ2IzbVNWOEhhUHFDRE9MWC90TSsrck9xTG5BUmRJaEZ6WUxXeDFCQWFvcXlvcytNVUk0OGsyUHRIWXJvdGtOOWtZUnZaYllzYWZyNzczK0tzdGZTZHk4eWFvQ3BCNmRXQmswKytlWDdLdHM0NkpBSTFUL0prTGplWlE4TUNzSFRCRE4yUWJ6eld0eG4xZ0o0OS9pK1ZZWXBZdGFSWFhaenRFY2hnVkhKekVTdmFxMmhTbFpTM1lQTXZhdGcycTdIUEM2dGVjTDZvTmFOTElNME1QdktBOENyWUtCTE1DRWhHNUxJMDIwUjFtMTY2SFpGRlU4dnVFZWZJZkRmU1J6SkxvTU5LOTFBZ0dkR1dkc2RyUTZNeHM1MEVsWnp0VDRqZHdjYW5aRS94TjkyaVNiSmRheTR0cG5raE0xdURFWkVsK09CemgzK2hNb0JVRVpSZzlCblp1Z2NPL0lMaWJHOFc3N0NmS2Y5cVFKclRIMEhoaTd3SDAraFUvendlTUNEK0VsRlNWSS9iWWNZY0l5eFUvckVCU3VnRlBybU5DUXowdGZ0aHFLS0FRcVlsd0N4Z3llVy9NN08wN2FSNnBaK1RmK3ZaWVViZnBZOUV2WHBqTk5ORkZ4ZCttSTF6bzhQdGpVVm41Q2NJNmF6ZlAvY0RZY3BVZU9vdG5ZcE5OSG1tMGc0dFM4b3MzSDJPSTZNMUVaTk1xWUZZcTV2MXFZUFZoTDFFZitLZyt1dS9ZYStpM2ZsNzJ2RHJYQlhpSEdnNFQzSEJIMERwY3BYK25YM3Y2QkJjYjYwQ2RoUU1LOWRxeXJlN3VMNjIwMmM2ZkR6K2svaC8weUNqL3JmK2xtV1l6K1B6M2Y4NjF0ZUFTQ05MQVRmRUM4dmhZZFlmczlYT2tZeTJaZ0k5dXNFMHhoYzRrMmowclRoMVJPMXBEdUtIakVpaTZJbUtiV0prVmFUTUplZWhXTkVBd1o1T2REdkVmNzRWL2tJcWwvMWxzdzZUemRHMkx3cUpqSExSOS9EQlJRQ3pxTEtXWGxlaDZCNk41TlhTc1dEVElQYTJWU0xONHM0UGxudUNSTmx5N3poN0ozUm93cnJ0b2gxY3BqeVpjQ2EycHRNRnFvRDJLcnNpMFB6azJCN0l5N21oMHE0T2ZIZ0J1M2RwaU5HdEJrQnQ3TUZ0NVU4NmpQaTBzMFQ5UmsvZUJoa3E2VkZCN053S2cvR2E0S25KRXlUSEVNQ3BQby91SkJwSDIvNkNmeE9lVW1ENXdla0hiUm0rSUNCakdxRTIyeDlleWNNb2trM0RaamEwU1pydWNMZG9tYmRHQkVIUXEyNnEyNVF2c1M3ZnVRWU5PYUpQTDFVbVFYV1dCV1UxT3JvM0hMUlROWGtUNkM0bStBVXRMWVYwNjV3TnN5ZzdhdXBFUXcwZDVWV2JJMEM3OTJxRi9JYWhtWUJlbUZZNVh2YnNWaytkY3RuTGRnNlJmNExDRU9aR0prR1JISHNWUVlVczV0dFprR21XSGdaeDRidEpaL2EyWmJvTUdxQlBoc0Z6bnhqTGpUZE1iUDZsUG9JV3NxUFFTQWdDVkdHZm9EQTNQN0ExOFdhOXdVclhLcEVFUVhZamQ5amFNOHVqWURHZEFUeW1YN2NpT1ByaWszcDN5RG1wTUNyTlA2WXNxNnQ3cmVWcXJtN1pXdUhySVExOExsSmZhOGRlMm15ZFIvRjJnQWUxSFJmOFNnb21aUE9LSitWWFdjS2R1cVA4dHBoLzVuY0NSWVBWNVNNVUlxMlpIZUR2U09BVmpDWGJwVFNVYlI2QzJuQi96ejV3L1cwRkd5K3FjTXUwb2l2eVcxcXI1TEt2RlkwZGxnckRyMFZSUThCMDlvUW5IZS8xaVVDVFZwWEdsQ3NHSWQxZSsxS1dBWGRIS2plK3dmcnM3L25PN082S1JsSjFtb3BEdVdMckFSYlZkQUtoTXlOcjIyRWdEMm52UjNQWmxhZklxaDR1dHdhZXFYbHZLdkk3V2JWZjJqcDRtQnZjV1BCZ1QzT0tOcHZCa3M4UWs2VVdkUkJwdnRST1JXMHpWemR3b3VhRnhMUWhVMDlMUzI4TjhoWXV6Ym0wNndZcFN6MXBBZnZ6b3RJZ3c5VjU0Mi9oTnUxaHRtRVBiQzBWeXArWDdDSmhaRERLYmlna0E0YTRERmpsOVBCbkJFc2FyOEFseXVYM2dVM05DOXJGMTdHMUE4Ni9RU3J4U3kvQzJYL0FNVDJ5MkdWbGZtRWZqaE94TkZrcm1zd0c1MFJJRndoOVBnb0tXOUdkd0IwNm9FWkFRSno2dlZyWlRXV3d3c0VPVXJnQU1mNUhrdVNhbTdMZ0tBQk81bHJycGxkZlBqNVlVY2ltdjRDTzd6ZjFkK1dBcmUxQVl3SHFPVU9SVmpLdlFGb2dxTG5ZYWMwcXJjdDRmNnJOcmZkMGJNZG1LTnlwTDVnZmxHNitmZHBGaDZaRVozaXJtTGMwcE1WbVlQZmZtRmJ2ME02d0ZGc3NINWQ3QWJXdzFzRkNtWjFCaWRpdTdSWkJqNTJqTVhzNUZ5cllXQlhFMTRFQjMrWnVWZTdEMTdLamQvUVMwaVV4bk5yS1NQRHp3cUpxZzhBdmRUaTZJYytZcXNMT0NmNUZsUm85ZHlLNnVJOEVCRW1pWW90NC9xZkFKTzEvUTIyZ3BnemJERUNBZXF1ZExMUkNBeUp3YmtZNFZRdzVoVVRHOHE4Uk5pRWlTYXNFNFgxUHlEbEYwbjF5YXBUc3VvalJ5ek1ZS1pQd054eXoyeTgxMktKMWV5MDluWEtCdzNKb1NMNEgrSTgweVNTeXV4eEpoWUxZS3R0eFdCVDREKzNnZCt1YkJsc0pwYjBqVjNEYWVTZW5WN3g0cEs2dC9zYlFvVE8xalBDdjNGMExrdFJhQnIxOGZ5NEhoK2xRZ0oxaDBuaEJxQnpQOWtkNnhwUFVRU1VZVU1venI1ZHN3V3Nld1BxMGJ3QXlwS2RMYkpzT0M4KzA0QUl4M21ycFR6UUxsalBpaFozb1BLcUt0WWlQeTY1di9YRWg1TURPQ2dpcHBJQThpMy83bHVIYmV3cTZzZGhIZ3dIdDNBMVMzOExPOURQcnU0dUY3cnFlVWM4eG4xM09lNlRWTUQzSmJaVmVubzVnaG1sakVvU0Y0NnNDdCtRYnhKa2xPQWhyOVY5UFRyMUpBcU9qWEh4UjlkZWxSR0kwZWtOUDlyQ3R6N3hJZnJUSTJOTnh5T1dXWXhneEtxZW94T1IvY1lVMVRFTW9ITlVKNDF4M1R3OE1YQXpNUXVGWGVkTTNJUmJ1YXAzOXRmOS9SUGhOY2hScVA5OFJlSGYrN0IxRVQrYW5MVTY3L3FnODBsbC9VY2dyK21GL3RkZkdtbG9hVGFrSmVQT0VPZnptSUYwZnFPVy9lbDdQUjZ2THNyaDJQcXFBVXRueDVOL1BuSndpTVBJakU2S3hGYjJ3UWxsRFo1NmdxZU1sbzlUVEJZUS9Zb3RkS3V4M3pmQkw2OS9XVTdoTGQ2Q1U3ejFabVBJQ2xPcEFKVVBidkV2c3h4RzJyWXNtTmthVGZOV0JSS3g2Wk5kbmxBUW53Y2xEWmExWjBUaGREQ2hkRTFaSDZuR0RPbTMwbUZEZ3NneTdNeHVXTFdCQlFGUGVSdGRrVXJMMDBqU0ZYRWt4NHhidUtuVERyWThEcURUVG1JczZpWXp2RW9nbytqOWpoZnA4bkh0NnJDL0dTNUprdTkwbWppMkl3SHhseG5Hdjd5bnNIbTJ0YlBkczE3dTBmNHhlcitPWHBoT3I2ODRnQzVLYkRVOTI3cGhadHh5ME9Dd0lMc1RWVUxFS3A0SHd3MzZpa3dCTUpacmRDelprQlJJUm9sQklWOEFOVnRHKy9wY240cFlqRCtqNkFtK1hOS3dSVjNrYmJtSGxSSE82eGdHbEJqVlFCanNOQkJKVmxXNWlxcTBRWTNXQmxqZDFLRWRuUDNxR2JxeXdZakZFRHIwTmhNcUpOQ3Y0TVRWOFVBdEE4WXFuYVNESnM2VVQ4RlMwRVFKOXk3NjFWSXlzQlRWZi9xS05VRlhaelNSZHdFT1ZSZWQ3QVNMRHVoTnYzd2E0RUJ4T2JBbjU5UEdLUGVBSU1Cc2NXb0dyQkZpRXA2WTBuSEowb1Jsdi94UTJMNmlIUUdzOTNKaTBsLzhqWXZaaC94bkNRbzd2SllqOWV3YkxZOHVPSXdOVHJpQmIvaWZCQ1I2K0d6WUFaR01TK2pIemh1THhSMWF2QTVxaDd2WGhlcXZTMkVBNHpFYzhmSE9IaWxxYXM2aUk5M01LS0lYYXBxaWdLaDFwaHZOSll5Y2NiTklGb2NmcDBBVWMwWmJlVEtXd0dkUGtQaXhzTnJCSTA4SDA0VldScVBzYjB5ZmJPRXI3RS9aR0lhalJiY2daeWJFZlhXKzZzQ253RHFOaW5CZ05rZXNaN1N0MndzWmZYVEZUUXJUeWZscWM0M1VIT0VDZFBSSGdJL01QZnc4RzJ3RkFxa1dibUQvVnFXTHFJVVBsR0RBWlZhc2dYcGY2NXJ0N3JvanhnY1dOT0pyTjFHVWRsNGV6TlZwOTRKVTlDNW9aeG1mSXpoQ0lCOG9xem5QVGk2NUtPS2YzUE9HV3JDc1F5amdTRnhMTGdNbzRNazRUY1VtUDkzY0JsdUw4K2ZGdGNXOXhWNmZidVg0NFBoUXhEVGhOdjdTNnRMcG9YOW5xMnozUm5nMk9JTHIvN1pmOGd2OXFJQTd1NnVpMHhHZi9FZWxzQ2taK2V4TkxaL3FyY3YzMk9mQlFkRC9TNG0yN2VIMGIrZ1hUVnRWWmdmc29SYzN2TlZ1OFpaSG1ZMEE5NE54N1J6VjQ5d3lvRW5scWVEa3lZN2ljcGowVjF4QktTKy9vNDRVQWlrajY4dFFFSkNobjE0S0kzaHdLbDRVZkU0VG5uNnY3MGhzL2ptMkVtaFA1bDAxdlBRY1EzT1JTMStJaVV2Z0J0MGw4U1FGZEVHVk11YWorUVVFZU9hTlhxeGhEV2Z6V2VlV1c0R2VBa2NDck5tb1hsNWIzNHZreTZxOGpoVGJFNlp2T3hKMHcxVUgzclZzcUdQY3hDd0poL1pCQUxpOUhiTHkwNHVCS1pJdXlaODBkbzIyYUFEU3ZFMjUvWjlLNlB0aWo0dzlTSXBqdG9FNFhTODNHcDVtSEN4d1NEQnYyQWJ3OGFiQitaOXVRbVQzTGJvSFNjd292eklJbllMekV6aXN4ZjVHcGFsVWtvbGlhaTFpYnVINjJUVURSdXFBRktVUS96YmMyTERsTDhzTUZRSWVFQmlSVzVkYkwrSzZSUjJOTnovYTdhQVVXa08zUzBJMEpxOEpISHZUUngzR3RWK01tVDlTeTBJNytIcW9SV3JYbk1JV1NpUldiR2NxNmU0VDlEeGEvK2VYbUtQL3BwV3VuMnZ2QUVPV3M1RHZjTWFjZUFnZEFXNHZTT0lsZkREWUgxaVNTa2p5UVNiYk1ENGlrdHlReDE3QVVpZnF6bTFzeVZ4UE1ZZ2dRbnBLZ3U4L3Nac1FYVmdoV3M4ZXgwbFd3eVdJa01sNnpqVGhtOWd0K1JqYTdmODVTUUJKalZheUdzeGF3NHVqKzlBak0yRTNRSThNRU5KcndiemUzWVNrVFB4THRTRXU3ZUdPYWc0cHlvaFdvV3AzUTRJaEgxVlF3QjltU1RBcll6V0NWRDVaYnlGTWkzSHdmWksxZkEySGUzdG1rTlBQbDdBazhwZ1RzT29nQlp3Y1IrYUdLcWlwR2F6aGtmYXlQSFR5NEFiRjB6OSs3NTJxZHR4bTZWenMxQ1MvQTlDUlRqZUNucXFQR0xrVXdMVjRpc216ckxoZ2V1M3RtQW5XaGdGNkpodlpETExXdlB3KzlsSktHZHY3MGc4bkpXckdZMHBOVlVkTjUxTUlZL1pXUE45SEtwNHNrSHJxYVBjaTVTc3B1cXFUUE9lb3BnME8wdXhhUGFNSmlKODVtYnVnTjlEdUpPbVBUTjdIVmhmV1B5ZDJaNU8xTzhGUGk4dzMwUnYzY0JOeUlQOEQwaXk3MFlDU3RVUURBQitDQSt0NjBQK0JScEg4bnFaeEZnQnY0QzNCaU4rR1lPdTVVYUR2ZlFRdHl2NVBFc1c4Wno5Q3dRVml4NDkzVHJLZXRsYktpMUZOaWlNeHU3cm9qZXVYT1dkNnJEU2tHVlV4MmZPQ0J0MFVnRmNsamI5enczNjR2NWMwS1dadVhMSXRXYzdyWlNxN0tjN0NxOVlQUzZ2ZHN0eldQVFVWT1J3ZDJiMGx3dHJjMXpwczZNUVcyWG1Nb2ZzMGllazRaRTBWRXZBN3hRaThoSHh0WHJYeEZPUW1FUWlRRnBrNElvTXV0NytBZWs2Y0lncHJqVk5xRVowY0ZGbVE5MWxDbUI1R2MyZ3dnK3BITkJBZHU1SFVVMFQ5cmFXbGR1dytoT2xVTi9VbkdJQ0FDSWd5bGdVNEtlcFhxQll4cXlWWXE2T29wOVl2MThJOHRDbG4vT25zaEdwUVFSazhZWVRxcTlJR1U5cHh3NEdBWVlHQmgzV28xRVJtNFFjelhScUdLSFExNEpRQjNNTGNJT3VkRXhISHp6bU1GcGdySDh3V3ZrWFNUOXBkRjFSUUFycHB6b3dFcVhzenkzOXc2NjRVUG9oa3VYdWQrQkZYTTlLWEFKS0kyQjQzSHVDU0lmN2lkLzhtZnNWVVQ0NUNSL3JUZk52Zk1pUDdhWUdnYTRLd0VDOTdzZk4rT0dtVS9UNEhVWVQxdm9QL1YxcS9zWjd1SCtLaG1QaWRCalo4ZWdRUlpHWXdCY3Z5ZU1wVUZEMnFqMStHaHVydlpZSnBscWZzQnVMcVlsWFNkby95cDJJamkrWlRDclpZOWJOaTViUVVPMEpBK2lwb2V2WFA5anpvQW0raVFZb2k4KzJ3WSs0bDNBTkZMWWxjaFJXQnFldWMvQXY1WEFqcDJzdUJwWFFRVmE1K043YTlWZzNOTE5SWlRVNmUxUEJtTHJjTnVGVWU1Vk5iNjArQnBQcWJYbHVMRWtHWExvT2ErdFdOdVhBTEJJOHBVd3BUbm0wYWN2T2NGdnBkSXFZZGNpT045RUVmZTZHT1ovamVqWTNad21xb2JXNkFEWm92b05pZ3dBM3pUYTNPTk5sUXN0cWVudDEyclpISmMyVUM3TXFSMUFGZk93QmpubVpDQzhXN0NLdmJkZEZtQ0krYSs3dm92QU0wZTRtY2lmT2FjeVJ5YXdwVnVDZ1NBYnc3bU1UVi9pUzY0QTY5NExmUWVabHEwNXVxa0NMVDJydWk1ZXVkOUJodEExcnExMEtzbFNJbGFreHF4Y1NpdklRc2dIWHpqcFFoQU9IT3ppdFRhU29XR29yM2VtUUpzKzd0VVZrUlEzYlFsTFEwd1M4TjBoaEVJVEUvN2Zkd0QwdzNNYjZKcnllWlNoMjlKcVFpSkV5eEw3TUR3YVFPT2NiUktLQ1A5a1pLcXdnUS8rSnUvUDFSaWdBckZWZlFoTVlPTU03Q2UrL1NnQ0FZSmtMSXJDWHhLOGNpSmFxNVNYNkxweCt4OUp6ZHJjcFdYakFiS3E5NXhKVDNieHQyZXYrL1p4S1M3Q292U1Z4WDlEV3hTSnRya2poTVZrRjJwVW5IM2VtVGRCOU9HRnpLSWlpMEltZWc1QkJZNkxyc09rK0dBdk1lVGRLWCt4aUdUQWJKMWkxN2NrZlhubEpaeXJlKzRDdHFadWo5MDcraTJPQmRPdGZlTVBVYW1scDZabUJPUWlSK1F2N1gzM1lFVXZueGs3cDVpV3gvM0NUeG5wY0puTU1BbFV4MzBZeXNJRWd6aFdUSXF1aFdHTlNTWFgxR2NkaDZzM1hLaXkwQ1NsTXdHMjY0UVppRlI4R0RkMkQzS2NLSmRjWTJxSHYyUlNFS1pId2JtQTBrOHVBWmJEVm5uTkNMTWd2UFRzc1ZaSWVoLzhCUERjN1FjTVd0YzZMRE9lY2ZaUU90ZlNUdS9EaXMwM2dDYUZsOHd0OThWZkQwTjdrU2RkMWFab0labVU0bkcwUncySDFNclRRQUg2MVlPR2VJUkNndThLYzl6VWQwR3FmQ0x2enhHNDFpb05GajRvZUJoVWRFbTVoNVhtdXhDTUdRTFBzdzFPZWhMLzhKekg5RWpOdXQwdHB4KzN6RGdxUFJzeTBGb2JWSHd1U1Z6bkRrZnduODBiS0hITnc3cURhWGhscVlhWmNqRlZaTWdXRDFwY083MHROQk00K0YrQmE2YlpRTEwwRHE1blh6VytHNGRJUVcxeFpTQW8wNDNzNU9HUEs4NmpBeFdBamRSL1ZVWEw5SWp2ZUVVZ3RlZVR4RWh6T2dEbzI5dzh3cG5uQ2hJaEQ1WkRhem9vN3lPL2h4YUpkOEtLa25iNXR2OUN2SHQ2UDBGZGdBVHkxYUFhZTVaTGNjdDZhejdLcTZQUkdpU0w5Y05EUHdoQndRcUx0QXp5ZzFENWQ0RzNpZWpBVCtUM0d6WlorNnBDYXRNL1pSV1BVVlJaV3ZBWTRYNHF5YkVncDZuY0JvN3M3Q3U5MU5WdFBQMkw5b0FONUlDLzZqbWc4cGdoUWtaYW9lbGdUOUN2N1R5ZFZhNFhaNFBzSWF3V2ZHckVWaFMwUUcxN2JnVWt3TlRBSXFQVUY0MXl1cEhkNWFCZUIwVFpaTWJ4RUVyd083MVpKZFRhRHU2TXZxSm8vSjAxMkJ4Mm1XSWU5WnVCWXhERm03K1lzTklvQ1pXZy9wSnYzWFFpT3JVRUk1Si94SDFFUHNRaWliM2JNa0gzcjFNUnpuQTZ1Mlo4T2tPSUZuUU1kUlJlUWxXaER4T25PdTdBVHF3VVdEalpQSG9pcWZwd0hnOTI0UXNvUGY1cDdzNU1qKys1QU5OZFZQeGtNMW9kV3YrOS9pWTQzamFINHF2N0Y0eitHSkxnVjFOSGdzSkFvWWlreEtWby8xUFJMTFpkUTA0TVBFSHNDVU1acEVWY2Qwd0l2bzZqSkJCTi9VSDdCaVdjWUgzeGNROCtjeWh4VlFhOTlBa0pmc3U0dDFXVS9BWmRKV21VblAxMnJwR2c3a2JkT204eTRSZm1PaDlFTDJ1S2REbEh6ZW1BaGs4bkZxREZmQ0I1R29ERThoK2tmMTZ2Zm1CM2Zjb21IYmRMS1c2aTQ2eUtaN2poa1U2c0JkZzM4QlJYU2dMSU93QmZQRjBrNGdSc2RNMXNTeXNobVhyVUNDUVNRTktWMm9yU05ibFdyc1BudkpaZ3NtaTBMc05TUXUwenZZekhKeFhpWTdQZ0xRdGtudllxTTRkajYzN1Viclh0WS9qeE0xa2pybnlSYkxhSXdHTm1WSkdFQTI1MGZMRVIyTDhUY1QvS3ZCVzhMemxsSlZkNEFhSHNPQ1F5bFhSNUJtaG5wbUdzSEl3MXdRdEEyWlNqbFRkdzBxa2hkQ1Q0eEUyV253V2F4RDFRQ1REVlZWS3EvdHpwbFJwMTNrWVhIT0c1MnJ2YjZXQXhpS1BMUCtMckVJSmVUQlltQk5nVi9wNHBnYnNtK25PdURwbjMyclVDcWJmNTRyWGhtTi9vVTBjL0VlcS96aHIvb0VvNXVwL2dzd0taVTlUWEpDL05VSWo2dVJTU29LU29RK0xjeTc3ajUwNEpLcXZSZG9wT1UxY1M3T05IWUxoSndWV01zbVVRZ2ZsMlZBTjIvbkxlZ2QyS0hwMjhYVGRzYmR5aEJLSSs2c0Y2emx5eHRNSER6eXNJNHBRK0g5WnFXRk93YUpDalVuWTdvVHZ6UmVQTmQ1L3BqZGNuT1JXLzhpUjNMRGkwU1BXbTBqb0t1YVBwTSsyWjE1RUI4bE9CUG0xR2UwOS9TNHg3L1VVZmRNN0hVSW1BNDE3a3dkc1dxQjZ1eWlKbytNdXBHN0kxM2lFUjlFQkdmeGhRM0d5YVJHa0VtUGZDV3lzUnZmUE13YXhLZWRFd011WWJJN2htY042Y3hwSjBZVG1vcTl4V2IwWTB3QW5rZWI3TlBuN0tCNjFBN1ZEcXhsendydVQzYUVWTkM1Z1JaK3QyTSt1cDR4eDQ5WlZkeXRHZmU1KzJDOUxsUEpya0Qzd292Y1Nlem5iMEJUZSt1YVpEVjlVdGJSdkRKQlprWDV2THUwM0FVZUhLRjNnakFheEZvaEhaTFRTaTZ2QTdTcEZDZzlncVM1SC9ySGZvbkRyQjkzWE5Ob281M0s5b3VHa3ZkS0JJYUdPRmZDWVdzd2w5SmpTZzJSekRDNlFod284bEtxSDhla2svS0k3b2NRWG44dnU1S1lGejlZbXlkd0diajdtbkZNU2x6VmNFNmY3bWZuQ2N4WnJ1NDcwbnlLY2RmSXZTZmw2cDBWcnVoU0VXZzcrVGhCMGhkL0g1a2J6L0s3OWs4cDVrdC96Q2NQL0lsWDFaK20xaGY2WHY4RGQ2cGxJT1J4M3BaOHpzZ0RreEtjZCtPckZvUExXL1NWcnp0WEk3NTA1Q2M3ODRtVVdkREpPSm1MUWxVK3ZLcjNSZUJrM3FQUkhsdCs2QXlwbTBYNXY3UnlHdSthVk9WTnBJWFRjNUE1eTJSMlk3SUNiZmwzRVZnR1Jqanc4NVV6NG9QMjBrUDlqay9jMjFBdjF4T0p6d3gzcVAvNEhDYnIxSE4vaHJNd0hsTFFzRThNZ3BJTmRMQWo3c0d0SmI1Zk9lVGhiS09ZRnZqeS9mbW84dkErdUl2TGtQaWttTDMvaU9sWHFYNUNncXMySTc5eWZqZVlMYlczWVF6Z1N1ZG1uL3UrSjd4Zi8wbmNDMEozUmZoZjBCQlBqbWIwWWdvWFZDN2g3VWlaaFlxY0g5Z1U2WFRJY0swaFVJeTRIUlRwZHRtdVp5eHF6S2dpNXpOTFBzT21RR3RVUFNUS2pnTWx5ZTdGNHIrMG1TMG1NYmNZa1BpdEcrQW00ejRGenQycTBIOCsyUjlpd2llUjVPMlRZSm8vUUJwZVFvTUpCMkppdm9nUXYwZ2lPbVhicTV1NWowdVptK3kvWHE5MnFDaTFDeCtVbVRVZWU4R0pqMTFLRFZWb1lyUS96dzVHU2VldXRHOS9QQ1NrNkt4ZENwK003RXZyMjlyWEFVV1VwbHpoZ3dKRCt6NmQrMnRtdnFRWURIU0FZakprWTN3dlRGL053cUFWdVNNY0JkNU5VV2RzS3NlazJBM3dIdEwzeHA4clRRR3pBRGxWNU4zWm9hZmgrOE9NZENTdWZ1ZVNCWXhYSFFGcU1hd3BXaE5DZHRpdjJhMHRuRlllckpZK0xSMGxiZEtFOEdqN0ZLZlFJYzhZeEZleVRGYjVLV3ZWWlFDT1NtTzliODhoMGQxVkdNa0ZUTkphdTk0ZjhKS2Rndy9yaHlOR0VJWXUxVE5mUG1UQ0t6bmlsZlAzZDBzdlRYR3dOK1NZL1dESlU0QjM0ZHByNUtrbHErM2RJMGUwWm9aVzBiRUluRVRYdEt3MS9yYzBTUzVRa2hoU3JtQW1sTlUwc2dZOVRxb1pVSTRFRmlocVc2OTY1TVpoak9HbGtIUVVzTS9CRTg0Q3hoR3h2YTZVUkllSk1TMi9ITXJlNi9Vd1N4L0toN3UwYmxXc29QbXJGdjB3Q2huUGNaVDBOWkxlQ1NJbmZxSk1jV2ozenNLRFV6SUR6UldrZUtyT0RJcFQ5R25FMWoyVVE4dU9HK2h1RzJnRnRGYkI1eE55L3lKYUtwejREd2FpZXA3ZHJVdlpZWk12ZExPYWU4QmFnL3dwN1FqTDlIckRQYlIrZysxRzBZRU1wcXRiS3dPQUNEZW8xOCtnVEo0RytLaFl2KzdLaTRYUnYwSHFybE4wTGh6Y0FEZFQ4N2hxdW5Iekk2T1A4ejNqNEZSVDJQdWxIOURKR29yL2tsbmRkMHJ2Yk9hb2VDRUpoOWdocXhnMytqQ1FmMW5kdEo0bGZqQnNqVVdVWVNBUVkzR1I0NFQ1VEJwc2tBVC91R2xJS2ROeGxRbHNManFCZ1NqcS82dFRldVRUNXU5dEQ0WCtxNFk4QVl5NDM0Y3FCa0VtZlEvc1VlZVBKZXVqb21WUGpPalFqa2kyVjg3Z3NoUzk0VEx0YzNCc1NmLzM3NEVKTUFSKzFBRGI1dlIyL2xXdVJKYmlwS0JaQ2FDRWdpNEx2d2hDQklESGlTSW9qbXAxbXhaU2VwdFRHdFc2TXJHelZydXhZU3hDZXV0MlRrVjI0a3F3SUdOYjRVR09SRTZaRkQxVmVVdmpQUmcxVkVBY0lnR2U1R0xqcmUzR01kd21SeFl4ZmNOVWx2L0ZoWmdPL0hzY1RwQWJPOEVrM1BDSjM0UXlHWFBScmtqMytFVDYrMGU4RGlSUDdNR2cxcHNHZldhaTZCd2RHSFlmeE1wQVRaRm5URFJjRDlzaDV5WDlFTXB4VUVuMWR0Q1FWS2FGV1prcFBkcGJFMDQrRWVTSklzNWxEbEgvNnUwemZISEpWK2dyWjBsNWQ3N0kwUjJvbEhRd1N5WHNxTXFjVHhVTGpqVTJSY3VBbGxjQmFWd09ESDg0ZWFhOG5Sc2lXTkZ5NG5yS3FYNVorYk5uSTA4cm1jV1BDdncwVTVSTXJmMUI5TGc5eTA4YytURmt6Q3QzU2RUR0JsU0lSdS8yYzcyTGtBdHNFdTlZSk0zRE43eDlLUzNZRCszVVduanNxMlVxdG8zY0Ztd09hY1FxdDlHT0d3S3RMZDBnb3hmUjZBU3N4QzhSQWVYMHVTallITTE1SG9LMnJ6dW1uemp2SjVQWklwYmhZcDZCSE91dUFNL0ptZG5ObWtmVmo5ekhlejh0ZkM2dVFNRy81WFhRU0J2UGhRVzI2MUlIYWRia09ySlk3bVUvNW5DWlhKdkZNVXJHM1pxSWFKS2JOU3hneVhKT0xsVVpHdS9lcmovV3F3cUpZUGY3dkI4MzV1NkZMSFN6S2tOV1FKMnQ2enl6U08wdmFRR0F4dCtZWllBOUxXQ2RNcmhiak9NekhPbzQ5YW15QW0yaDNySGhIVTZhSVRaSEFTSXJaRkwxT3pnR1M3Unh5VTdiemdRUjlvcXp6U2l3V25NdzBjcVY5RGQ1eG81NVlFTkk2MlpObUZleGdxYzIrRWdObXAvc1VTM2orelQzblBXdzhEVlFkc1dUcHRuSmJycDIwZlhjM05UVk5DdGNiZDNxWGVIQ1JZRG1nakhxdHhuQ21mYU5hTWgrekhIaWQ2cWxINjJUNG5pT3ZkZUcxV2NiOGdCazNTTEZvaHBQanRwVVFzQ1lUNFBFUXlxMU1BOFE2d0dwQTdSS1VtS1IyY21mZWp5SUZOWTBEOVFSdTlQYlplVFVOR0VFN1VaL1BRMzU3R1V2VWd1MmxPUUlSdVM3ekwvMGtyTTNYa2xLRjhha0Mram8zZ3cwdkpya25BU2F5R3M5cXFwbEwwQkZ0RmJNWSswMEhqRVozYldOYU80L0VZSnVjRUFFNnVraTYzdVhGZmxDVVJmcTBQY3NEZzdIT213cEVTVWVzb1pudEhWQzJXWVduSlMrTW55L2tuZGl1dWZ1cXhocVpTMzh6MW1nTXhSWUZTMzhZZ3RJQ0Z2WGg0c1NJWWViVS9UeWpTeVk1NW44OVBSWmczQkpDS05IbWs3SEROTVA5bjB1NzJndFU4NHQ3N3QwTXN6aXNnOGxOQ0ptS0E3Slptd3pzWXJCcTI5UHFUazk3K2NTcE5tTmd4TklSKzhZaDNxc1hEQVEvVWl1aXpKaG9qZDZHV0xqSHEydU54Z3hMZ0QvaGNSSWZ2UVVQR205d0wxdC9IUlI0Z2FLVUJlaWdhN2d5VGliM3J3NnNHalUwRXJDRGpKZlVpQ0JvYWZXV3doZFBnNzFzWThCWGdCN2hPdTZGTXlrZVpBN3pNcksxcUJWbk1FNkdpYUJYd1MrcUxpWmRKQStyY0ZyNEtVR2VGMWNsN1RNc0dLZHFJd3VSenB4RThZQ0xVSXhtL256MGdOMEhUaHJQQnc0TjdNaDcxZjhhUWx5S0dsUTUvT1o5SE9xZmNYRThjN2tFdEcyR1dsUFFZNUZSRjRqUjRWYjJiaHYyaW9qQ0lqWGhtVlBGYU5OWmFHYUphNTMwdFpqdkVpVWJKbHd1L1BRcldqQmlIcGh1SmpodVM5TFB6ZjAvZjEyQVZYb2pvSDU5S0hMMGI4WXdMUUd1QWhkc2JzazVPbGZEdTkwOWJ5dHJsNkVDOXdoTHNLem9VdmlpT1JFTlgzSU1TL0s2TjdmZUljRmJtMzhhTUI0TXkxcEhRTnNISUY4bllkdzZWQ2gzUjVEcmZNNnVzeDdlbUlVUTY2WGlEbXJmUU5YRHptRDJsQ09LNXB4TVZpOWduYWNDbTlvcDJXZDZnQkIzWXlzNWg2ckwrcWxMSUFnNFBORVY3d3pjUWtINUtMRS9VWDFvYWpUOUFNU2tuNzRlQys4TUU1SytYZFBxb1laQnhTWjk2S01Pc0VHcVRVVUI5RTVsUU94a3JmUUg2WnVnUXBCdnNtUHBjeFdPUnBNSTExcThkU1YzOS9nOWtHUFMwYXEwb1hMU3dFMkxRNEV4TkVTVEl0Y3MyTkJYaUFOYTZEUkExQ21PWWkwU1BHaHRlbjJHNmNzNHIyNWc0TndMa1orcloxYWlWWUVTYmI3L045YzlrWDNpb1l2MkJYdkl0Y1RWZmtqMHlDUGxydE9rdVk2RDJwYXNQL1VUTklIQ0lha3JxOUp0RmMxSUxtelBoRTFDaE9BYXpQdGpZWnR2Y3FKVks5a2dta1NkTlFUM2NsRG0yQk1OZitLMjBlam40Tnp6Y3YzN2pIdERqaXNQQURpdWhKK1pXMGhOU3IwblVnakNpL1hoSk84Q1kzY2ZCK3grMFVNc254OGtBSlREWlRVSXVkWVorTGVNb3dNS3JrL1JLV3ViUkZ6UVR2dkdZNFVTZ0VpMDViTDdGckZMQVpVSzFibkFuSHoxeDZiQXBnMmFCakVKN3o1Tyt1U1pnTnNpNzNMc25LKzlhWjdremhvQlJ5Rmw1YjNnSndBYlVELzBxU3c3U3lORUlRYzI1N29zbURYRHhmbzNwUjN2RXhqRzkrM2k4VGNYWGxjZmhsaFM2UVNqRTFTKzdKQUwwOW1pSGJ1ZkZpZWF1UUs2UWd1RVBjcUE4TzZGZ3EvSm5zSjU3WFBnMVBBU1RNcU5pUHo2ZUhoK0JoNzRkd2oxUi9LWE5TTTViSWs1eVR3MERGdFB2Y0dSbUNrUkw5ZGdpMTdmQmVzQlpVMkVUS3RpTDNvN1dWazdZdTM4V3c5TGdLanVIc2JMWkF3cEd5QVlmSU04QzhNaC96clN2SG5ocG5iVFpRQnlNT2luWUJuWEQwZGlTUDRxZDBnNlBBZWJROUlpTVJzWWRPQ0o3bzRHcDRxRzcvTmVaUUtTRnFxMXFrWklNQjJPcUk2SjBLaUhEV3h3VUNCcW9UbHRSYnoxRDdMWWVENHNjOG4wVklPT2ZLZTZNQzF6bHAvei8vU2FVZlJKdmRLTjdLdk80bUdlOHVUclk4ek5DbDEzNjlWL1g5VlhIeUM3QkIyMHBxVXhwM2JxeWNlellQNUNSalBsRGpXQnlWL0hMK0x1UkZsMStDSTBXcWZiVVJSM0ZBQ0FmVXVSeUQrTVVKdm1oTzc2NWZ2dVpyQzl2czhzS2trNGlyMGx1YWMwYS9ROFk4SzNMVjdSODhRbFQybVA5Z24xZzREZVQ5RTBMUjlFVWtQM0dneXRNZEpCWWhXajZib2pLRnR2Nm1OQlBZblhkcWVxbERWNHFBQ0dibHoremZvb1htc0FXbUsvVklxbUVCQ0VmMmRVQUJUaXFwVThhdWRkTHAvM0tmSUNodWcvOHJWa0FleXlYMkdlS2dxYTA4amE3UHhrYTNLczhzdllJTlhmbWZrQ3RCcDdoMmJMNzQzdnJlZDgrdWp4b1JZNzJkaFExSkp2VjY3YmxMbmVaSGIreEdoUjlZNmk2VzRuT0JwUjI3QlJuUEpwSzhBSW1DUWlkeDJNMDlKZ2I3eDRrQ0lmZUtEMXE5bGh1WDdrV08zYU4zTUFCMHdxclBGSU5IMFVKQTF0dS9sYis4VVVGRjRzVTloQWp2aW5jenRVSnB4QVlsUUxyL1NTQlFDeHc2eDRObHRaTXZFV3BIRGtZdk9PdEdKQUZYaWF5Rm5oSmF6Skx3Sm1LMW1RcG5rM29MV1l6ckdtbWlOSXpmcGNjVFNzTEo3MnhsN3hSWTN1NXdiV2dGZUVQU21NVDFLbEJwcFZ4aFp0VVUvTCtrNzRrckJ6ZEQyL2JRWUc2UzFEbURjdUVHaGdpNzJCczRZQmhwaHRxdWJRaU1lT3FvVEh5blhLQ1ZZa0c0Wjk5ejJyZHVJL2F6akljQyt4ajlCRHVmUWtJandDRUlvRFcxL1F1TGFJdTk1ZmIwNkRxbjFGRVZDcExkY0srVmN6a0FoRUdtUklsQU5UcUZQMGpFTy9rYlAvam5PUGxZZGE5eWIzZXZSczJwbklmdWw4NjFuNGxHU01SSUFhcTkyRzlBemVhVGxKU2NXYW5SdGFWNW9xSHhnRXBVV2YxWXBwRVc0eVBiclBlRnhUdzUzODlvNWozVjBtVXNzOWlzVHUzSDJJa2x4MldENVZrUEx0cXdhdGo0dEVDMFFjeHhJbVhMb1JCWXkyb2hCN2JTZVhGNm90bzd5aGlUclJCekN4RW04NllNTzNWUU9GZVBzdytQSlBjZ2xiTlJ6ZUVwRW9oNnB6UktldjlYVUxabFFKazJ3M0EyNG5aQzZ4L24rOFpkT25FQjNSNDgxZ3doNmFWMGZYdjl0ZG8zbEhXamRMalFCd3kwTGQ0QVZJVUJLQS84WHFqcHpDbFVPL1dmbWVDK2pTY3VFeXd6cW9CSk5nSWVOR0I0eHI2QWo2ejZvTVZTZ0Q1ZkxrVXJ0MGhYMXI3QkVGeURFdUt6NGNpS0psUGJidlFrOS9HTDZLaUdJUHQwY1VudC9LVGlwUjJ6eU9FR2UxK1ZndkI3U3IzV1dmaXFsUm5rc2IvTVRGb2w2Ym9hQzhtT0N3OWJFZ2xCaEZVM2VITnBTc3dXa242WjdkcFFGdnNqL3QrQyswM2N1V1V0MzJYRmw3OE9xWk1kbUxUY3QzeEd1WU93N3VUV2tORnFRYnQ4NjR3alNIb211dGdqVnl6RmpwbzJYR3pkMitpS3Jid2doKytiK3MvUEgxTks4NmcxRzdNdU1PbW5mK2pQUFhyREVhQk1yNDYwVGF0MzArMkM2cGdzWUdaeWl2dlFWeHlZUVBmS2RQak8rOVFCM2tudkhlVXQ0SDEvQzJ3cDJKck5NdlJNR2lVbkorUVdBRm0wdGFXejNZczVjYldia3BNQjVWWEhGK2lnM3ArQ0ZiNGtlOGJsOTRMR245Y3BIcXpMeEdOcXh4VnlSbDdGTW5YWEFWczN2NGw0Z0xnQmNFNmdUSTZqQVlKeDNHS2JPQ1ZmdVFZWmJDcFFuZ1ZBUkhiaE5hUmk3TWRTMTg0dHBXZnJaMEN6aUsyTFNxOXdJNFJNQ1hxUzhGQkRWMFlwaC9oNDVVUW9QOENiOGllamlwTUFmMHBXa3cwVEhna2VLNkZIbElaVDBOSlFNL253ZVZqTG8rQ3l5T29DZnJvSVBwT0xsTlB5NWZiZkc1ZUsvQVdyb2ZaOE5Hdmc3enBIdkd6OVNKVWYwaURqSFJEaXZ4THZ0Mk9XN3RmcVdGcXpmTzlzZE5ka2UwZ0Q3cSswL1pwZ2I0Q3hIbWhQcWkyMzNDM1NMRW4zVWl2cnd3QlZjWEZDVnFLVHJYM0M1bkhDb0Y4S0hwYU9RUTdlU1B4RmdQV0FxMXQ0WkFoaUNEWTFWUllRZmw5NDV5LzV6U3NPS2dsd0xxQTZUVzlaSlRGeHNHaFZMcGtXZEtYRHI2Q2RoTklOUW1qSitGSTEydVNub1lRVXplMGs5Y0FnYTdJaThOb3k2ZkNlbnBUditNWkRNZ3EyS3NrYzhETlNxbm1mamFjS21EcmlsMTdwK21aNUF2MFdtWGgyYWovQ0FuUDlkTW81R0NjK0pYM05PQ002V0JxZkRGRnJqMlpUd0VrOWw4YTZ0VEp2YWtIZXVMazBQd2tjUWdPM25uVTRTRlhxd09qenFvTXpaRTdVcmgxeVFJSzZOS0UvZDR0UitzVjlXQkJEMmo0VVBROHdqd0R3RzdxelNGNmxoTUpCcTRlNktTSkV5Tk1QZWl5VmwrSTJTemZyMzJ4SEVlenhSemhYYzhra3ZlWXkyaE1YQ3YzMyt2Q2RLN3pjZThNZVVoOEl6TXlTNnlPRzZuL04wWHdVdTU4YXNnNmpnbkorZFhpcXBnVkRVOWJYK0ZsUUhSQzhaM3oxOUIveElyQ295d09TK3Jpckt3NGZhUnp4U01WMzNSUE1CL3ZQbkRoTjZMcmtBQitBSkxUbDhQb0Zsb09nbzlDaW1PYStGTEpOM0JSMEkxakROcjN5L0k0S1RnUCs0YUVOejZkc0svYU1jK3BzbnYwTUZOS0l0blRrbHRpSkQxejBTN0gzdmdmOWc4REE5N1RSWEV2cXNpRUJ4aFNjVTJDVko3ZC9yN2NTS2dMTXYrQWMwTHM2WHd6MndiWmlRTFg4N2RwaWtsMjNQd0ZzRXlESzh3UDVGd1JSVERVV1ExN0llcitwRFdjN3JFd1dtK0JmVEh6UWNCUEtHdUdyUnBSaXEvaThDY1RXNG5iSm51aS9oOGNYOGxVN1V6dGlCRUllZGltaFNCeE1rWWVKdWZPL2FUb25ZYnVyRVRUV2RYd25wL0QrTDl5LzhiVndrOVV3K0JrWnRMVGphUENZUXRJTUdYYW5RY2JsZnFjNEdTRmhBSXhwWlZNTVREQzR0Zm9zaUFMU1dvdjZyTjVoY2hGbmJRVkErMjkzb2JRMUNGYVBtZmQ3dXNUWEZjT1lQVEVYOG9CR1BBN0NVaXhiUlc1YTFDa1R6OFo5UjU2T25IdlJ6bEtmY1hNajdUMmRYMk9Mc0FoTWxnVGN4WEgrbmk1cjdjUnpRU3NEV2xpVnlYeDJUa2VmdUw5Zm51OVBvTmx6alhrQXRKbm9ITTM5RCsvRUtBaGpqMXpKUFd4ZThlOWhHRVhHblIxQkJjY2RHMjNtSzRCVlJ5dHlQRXUwWnhQTU5oT1J3U2F1eCt0UUpSOFpsR1Y1SEhPN2JheEw4L0tRQ0xraDNORzZiS0x0RGN2dzc5THU4aFVhdFhQa1g4bHhhMHdubzltSnV5WnpyWnVqU0d4S3FicjRmZm9QaUQvSWJrVDBMc2NGSFg1NWM1Ykl5ZVYxc1lGMzNFOVVWRlJZOFpjUzRURjlVUEZTZG5aUzQ0OVA2YmRlU0gyYjBEOXpuUk8zNWRsYXV2M0xyT3RuQWYwVXdjTW1iNTJ4UmdGT3NZOVNhK3lQMktWaUJyUDhnUDREVExNR2lXeDJCUmQ3ODZSWTFGWHVwdGpQSkdzdk52eURla3hVZGhUdkZMd3FobVF6cXQ1SUxXTjljbThJc0RneUlWMUhVb2JrMjB4dmd5bmVyWWNOTFByNXRuYWd6T0h5R0dscStOV3gxT3UxM3hhSWxwa1ZmQWNXVVJVTWE1VHArUjkwUlhDUzdtSGlhQ1ZldldxOTg0Zm9tcDdlTEVRTVlsSnArTlhIRlpZMm80YWVVNjVScit5VEcyYU1taUIrVENKTVdGbDJwWnpIRFFuTm9rVXJnSVg5dDVEcUZQdmUrZ2FWYlpsL2FuQzB3OXB4eXZjK0FkeFZqMG12ejFNcWJtL1hidmlMM3ltY0dGVHJxRnNqNnd3MzBiTkkzeGlybHh5SFBuZ2ZGSGwxQTBIZFliZnJxaUVZV3JFZ1ZLZzFLT0dLUDFVQmppbDhTUWJPL3pvWmRqbTZBc09YejF2UkJSUGFCV3NFcEZXRVF3M1lHeThTZUlRNVdEOTFnLy9zSGR5TnJ0eEpVQ2IwUGtkUG90ZzFYQkRJUWVWQi9LTDNxQURnZU1DaWVtU0x3eVpuK3p3L1pPTG9Ka1FiUEg0ZGZDdXc5TXJVN1NXczY0aVk3blVDcDNLRlk4R3VuUHFBMS81NHdKZk5yTWV6L1ZwNGRFdG9uNGxocHhxdzRmL1kzbDJqRXVXeUVrMWRmN0NMWkJHTjlxUlhJQ3BoeWF6MjZHL25KNnhlUktBNDVYZFBpR3J1SlNNQUtkaHJwRnVHbEZSaFArTDU5dmZFSFJkVDRzRlZSS2ljakhpKzBhZUQ5cTJzVDMrVllxTmt5dWVocndUVy91SWpoamVhRldkNlJsTmxKVFJpbStwdmJwUFh2QTFaU0NvbW9OV09CVUpIVUVyNkpYK3VpSHY0cmZNaC84RlNPUnlrUlp3QlBUdi8xTkM0VG1vczRrRXBZY01aaVFzenNjQ0tJZUxjYWJjMGJ6TFJxNVErUlR1bzNwZXloSFNGZUJid08zdmdubjlSZTRDQlNZVndmdlZSOHV1Vkd3eFRpa0VlRnRUM3VjNGtvWi90NlVGZTM3bldORy9sL3B6d3VLRTd6V1NXSDRtNnk3QWZmMlZKVHZMeXRtWW9nTVZ5SHVNZ1pHZUU4dEFHSWlmcDROT1dmZVphUjlqejVzbTIxaWtTNjlpTEFMSnZMZnJwc0JVdjhzWmVWY3kwbFhVRmZWQ0RPN1h1ZjgrVjUyN0xIRncxSGV1bFU0VnZaMzZ3UHRpdFV6Z0Zxamk3eHgxMFZ5eVl4UEFoVklKbmJMeUg4YzJVYzR3OGh2MjVSWnJSUXZOODkzSHNSNkIzZy9DN0tucHVjcVNQdzdTZzU0UU1JNmpRNzFpSm1DUXdaanlKdS9tQzY1QVRwM0VVcmtIMzFDd2dQak1RUzgvbGZwaGpKZlhRdHFZUFBVQmNqUXZkT1VUZXJ2S1YyR3I2ZGc5bHFRV2hSSmM5WW9uYzU1SmJNbFZTRFRSZG1ZSithNHNLSTIzb0VJY29OSFkvRDZvL1NsVEJ5dmFEZVdnTjJQZEY3VlpreHlyeEdheXhHcTF5bWZHVGEzcEtYNzl5SnQ1NkhRQzFieXA3MzhiNzY3aW9qU3VYTmhXZnA0UTgzWmxVOXZoVnVUTDRaNERjcWYwelFScHArUXhPOW5RV3lPbVRLcWcvL0c2d2pxQW5FQVA1NW5XUG9SemtXQmR6V3hMTlFTWTVINjNkR1Z4dGRNTDFOQmk4bXJPY3JjcGhVdkJPTkt0Z1pUSnVabWE2TjdIMHJ0TUFCTHhUY2JxOTNVNmNiL2JoQnVFWXlhdlNDZGtrZkZRZ25SdG1CWENrWms0K1hUTi9xMCtJR3RDZ1pGcTJBaDhyT0VxaEc2aWRURWtMREdRU0ZaMEdqaitDTGJ1ZXQxY0JMUnlheGN6MWVHeXhqdWhpUDhvV1YvbXYrNDVBVWtUSEIyTGdhZkUzSjdzNC9CbUtUZDBka3V3QWNlM1NhbkhDMlVWR2ozL3crNG82ZWkyNFc0TldQQzBNdlRIOTBQODZnblVFYnJ1azJYb0owa2JoZXpRdkpJaStBakNYY04rWDFsbWZtb1laWFgvNXQ2M3VmR0tYN1FWbEZoUWZzVFVhM2pIcWpDVHh5YkxSTytZME5Uc25PcFZ4TWJ3c0JJdVpmemhqRzhtcHRvY29ISVN2WFoydStGV2k0eDZWenFZMXowaUE1TlQ2MkhlRkJNamQxTjdacllmMlRoZnZ6ZHFteVlWSEE4VmxHQ3dlVHdkZ1doNEFQa0xndUhkRzRjUDV6L29NTFRDOWYwaVdSZ2pNQUFlOHdTclM5YWc1SFdzYWU1d2kyR0U3M1VTTDhHbDM0cVQ2T3N1Rnc3TlhuMkM0YXozT1FmWU5NZ1VWSXdnVTFFNENnZDhBUlpDY1RWUFAyNnZmMGVGTUlHcXdvVW1nb0Vmc2RhQVZNeDd1ejB0KzBlNmsvak5sREtTejVWa2syNHpHN1N5alQ1MkxVMXlPV1N4RFVqOURPMHdNT0FiSTBwWDF0cGM4T01aMTNsbzhRUmFTZkNhdEVUTEZkRG5ZTGN1Q2pTbEVHdFlLRlFkNlJnTzRSWEN1Qi9lMHZoMmZmRm5JOEpTUjNHem41WHBIcERYT3lRNHVsNWx1U3Nub29CNzJaTXhDdjdwRTRZNkp4K2sweFM5NDFBaDVSMllmY0dqVDlOOUlKdURrMTF2NnJFbFRPS0F5QVFFZ1B1KzkzOG52OGxOeWdVcUFpVmZPaU0vWTBuOE16S0NnV0srZ2VWMUY2MDhLMnhjUkUxUXJveENIRndLV1pDMy94cko5dkdQWlNPVjdhT0d2Z2FIUTlacG9vZTd6bHJFdXloVFBKNkxuS0sybW9pQnBvYXo4elZxd1pvNmN4NTkzaG5XM1dXNm50OGI1VndWZW54eGo1K0RtdlhtUWVERFY1UXdLSHd6OTVEKzk0SDY2MHZ5cmFTMDdKTEVhTFVPU3pTR1BXMDR5OUxjNERyMGhMYVNBd1lOaTFoWjdBcklxdU5xeEtRdUlJRlRkc0JEL0wxcmdIY0R0c05KOUZhR09XRHYyQ1BnNEN0RGdDSk9sV1Z2WnJRZlBMYm8xNjNDT2gxZW5zZUdQTW5xTHN3a3ZVU3BEeCs4Nkkrd1YyVGw5Z2lIMEEwOWlZRWhTQTA2ZEJ5TmxXMjE2MXljK25HVWxUU3UvZVE3emVBWDhOTS85aWFVVGxDVnNiUHJiWFF0ZFRCUlFranRzT0xWVUFndmJ6b2JxUjVlTEJPaERHaWlHenl2U016NjgwR1FsMC9vWmxZa0tuQ251elU4eDE4UmZFTStHRU5xUCtsekhUMmFoU0NldXpzbVdjRG9sTyszTkpTOEV0UXR5Vk84UWIvQlk0WmNPcEdKQ3p4UzFkWHJ4QThyUnNVTjhGRTloS3ErMlpMRnFpdkRUdU1iN29vN3pycjN4Q3ZFNDllYmFZbWNCSWdqT0VnOW9PV0RhWEtRM0wwN2xGbG1WbCtjcnE5SFNCbUZzM3E4R0xHUVlXaGpqSk5KN1hhaEphcTBJbVY0b1FOZDNFc254ZjlEdzRITWY2STJncXM4dS8wZHlaWXJVendPb3pWQlVRMHJjNGIxWmtYN2lwM2xHZ204Um44Sy82cUZsTndvUm9jb3BiRktiZ0JidWdGOGE5SWtycFF1aXRjNE5IaS9yT3RpcUtURWtTdzVYdkhqTjBLSnRkMTJwaVUvR3BPQ0tOUzc3UUF1NW12VnhsWld0OHlEUkVpRTdvN09TTjJLUkpOcTBNNTdoY0ZLdzdUZjJ1TGs0Rkkwa3UrY20zb0lZdHBrTFNHZXRSMUtjekx6RTVlSlFzN0JuWGVZK1FmS3hOM25pNzNnSjJMWXZrbTVjQThRU1pjN243em84MGw4cWFqd1ZaRXRSNVdoTVJzTXhDR0YwU3dBelVIT3kxa3FKa0hrcm1SekhkUDdQNWVLYU03Si8raS9ncUg3V09kRUQ2cDRKS2hkOVVPL1NITGlhekxHdi9VelpJQ2duOC9PcmRoMlhyL2tXSUJOa25BdzQvcUFuc0NuR0NsOS9wSXBPSnhRbWdtSGREOFhlNkNPYStCNGtIVVJhcjYvSmRLYnEwdDhNd3ErckJubk02cldBQ2hPQUozUi80c1B5cnZhb0JVKzZ6alNIb1lvNG9KRmNJSGpTVVVENmZDS3JPUjgwZjZmbjY4TVRmVlppd3JRd09xTGE4TDZESm1DZGNrMlhQdlRwUVU2RjdPM2VKWmgreGR1Q1dieSs4STVZMmhIdDJ1S05SSVV5TVZ4NjZ1clMwenlhOW03OVpQOUhMdGdLR0dwdTJJRXcwK2tWSEF3RmVOMHFwZFFhTDVRSWd1LzJNNjZZd2ZOTURRbjFCSkFJS3RhQlpvVnluS1VULzhxN3BYWlkxRGtqZ0EvOWtXc2F2bFV1VTg3cWJhZXlnSlUyZ0pvSnpoVzBOQndzenVwQ0FIYjB1QXV4MkQzSVZDZDNhcFN0L1RIYnhjUXNocDJsTnlwcDlYSVpzREZQQko1M0ZXekpKYkFUYm1BdzR0eEtrRk9Nb3E3d3BDYmRVUFBxUC9ZMjJ1ektDMlVQbE52azVOV2I5TmV6a2xkQkVEQ1lXUXd4cTdWbXIxOVMzbUg5RFRNV0JmREtVRnFNOWpqUVk4aE9hckVha0NacndQbWowRG5lejM4d3drcXpwYnBWRmlJZTR3UXJwbGkyTFF0M05wdDJrc281NXMvdytnZlUyblNDQUVWVWVIWW9iQWQrTGdyRGt5Slk5aURJOTJmWHZkYStKZFNnSGVEd3hETnhlaU92aVhBaXBkSG1xWldaTEhyM211ZXYrejg4V2NZYlZKN2x3c293STlNdUF6UkE2Qk9RTGd4MlY1ckpDMkUyVFZEcHkvSHhkNXo4eU9LWVZHYUFjSzRxeGFhQzg2WG1JYXl5ZjRwREZtRkVFWEh4UGRKRGREN2NpT2VITjczSjY5eVEyUmhrN3pvNThVS2ZVR1k1UTRRTTRVa2diQnowTEpjNEljMTF3Uy9iNjlZa3JNc1YxZ2Z0Y1VPb29uQ0YwNTVpY0JiVCtPUWtDRFNEd0czZmhRY1pkQTllUzNTajRERHZuMFYrRGhsbTh0TldvMEhyOUVScm8vV1UzZXpPTVFzRGk0VXNkRFh1R1hscnQ1MlFiUkJmVGNMZmR4WWM0S2pvRVRDaGdnenRMM04wbkZyRU1DckY1cHdMWXI3L2NCbGdQZGUzamN4SXF6dUlTekdrYWhwOWFLN1ZaU0ZON24zZFRwb2Z2M2hDUS9mcGdDOExScTVjRmd5dDRaeUpsMlEvbHM0ZWZsRUI5MDJMVmViZmpyUjZtOG9iakZJMk02Mmd5VU9ITGcyeVY5cThHUDRVejVnZ0REOCtMU0I0T2VIcDhsVS9MYjlsUFBTMkJUdXhjYUE4QnRJVTRQOTY0V292UUlNN1V1S2dicWttTnlEOVZqRTNuOUJHV0Fidkl6SCtpQUtYbDRYd2ExeVcwa1NsMlpTWEVsMUhwcFZqOTRNTVR4encyRWd1YjZNRW16cXp1YjdCYkNBNlJhcEJuS2VCbFVhTk1XMGZVZURHWTJzUW9zT3JuZTF2RkpTZkROMGtFc0NBMFpLb2ltNDdLZ09kVXU1Y2FyRlIwL0hRSXRKb2EyeEF6aFRKaXd6V29wb0ZxOXVic1ZramRjUTRod2hIN3B0YmxmMmhtMVhpcFdqelB0b0FDandZNDc4S3pGNy9wTmo2MkFaZDZ3NVpJR1lIZFZvdHl0MFdXaEMwWkR0bnd5akhPNGUzanlWeGFDWWtXNWgraEd5cGJMRnhsVm9lQ1dqTDVPYkowaE8vY3A4enBvZTNIM0lLejNOQ0xqS015bksyd1liQnpITktuWEU1TGlva3NKY0dZaHpLdmE3dU9CQ2x3bEVNaUhYbDhXMUhzSCtnY2x4Z2dpSEJaeDFzVTJzdC9kaTBOc1dxTmpqZ25RQVNPTGIzWUljbEphT0VTTE1QNnU4M3haU2lFWW5ieEI2dUJ4WHpSQ05EY3NVS3I2Q2dBQldmeUFOZWE0WTdOQ1lTak5rc3JjQ0dqa0loTUthRjB3dWZxZEdQYy85RHRvdHVqeWttSEx5c3FPYnlNNG9HMFM3UUl2VElOeUVxODYvS3Y4YzgwR3VrQWszUXZ2R2hTaERRc01IU3ZjQXVLUFhEY1BZMXpvQlZibnlITVZhMmdNZFQyNGJuaERDN0kxdGdmUlQwLy9LYVZzZEt0Z0JIc2xKTDZwd3pSUExNYWh0SnBMUEs5UnVqbFB6UDVpS2RZdit6U0l0TFZHVVl0eDYxcWRzQVFpWHM2N3VyMTR6ejRTMXd6VWNORlptTm81Y0R6Mkh6Umw4ZS85dkRQMmFGTWM0R2ZTQVMzWENhOTk5c0drTEc5RHZOMkJhUjlZS1llendDSDcxbW9LejBGL1pyOHhhQUR0OFJpWU5NMHFReFYwMlJEK0lFeHRhVU9SWnN0WEVYQTgwaC8weFRja1owOUVBdlBVY3ZXenJ2RkpRRFNLMWhYSVJJVTN2bmZ3ZEZoODF3anJlSDk0c0FkdUdlVFczNVNuTGxtQm5SemE4ai8xandNajRGSFR6amFZYUlWSjVtcTFpemNGMDZYY3dJVUQ5S1pRN21PVFdoemtPL1JKbjlwc280dTN1d0VkdGVacnN2ZmZFVVZ4T3hZS0ozN0V3RlZzMHZQTGVzUHdNdzdCY0ZqTlZLbHpHV2ZOSFQ5ZTV2Y1pNRnRoaHR1OGVJTExWT0N4Z1JoMFFjQU0rYUZtdmFyRmxCR05JdkhnUm1TWm9YQ1ZuRC9ia2ZUc2VWRjBhUjJNTTUxUC9xcjBLNTkyZkFoLzFudUtZay8zM1JCbWc3WFFWbWtyalFKc2xJNGlRV2hQQ2RuaC80WGRYbW1KbVNvdmZOYnRBN0RLQUszbHNBQWlDcWhoOXoxQkdaNThjM1dlc0h5WGRCQ2l5dTZaZGxVdDE2MmErcjlQODhsbkZWNjVSYkFoek81SlhUTXo5QjM1cTQwa25Pd2IxdER4NnFMdGRlWGQzbUVIdllxYktrSmtwSUxsQTZ0UURaWmw2SndyR3ZmV0NyY2pTd2Y5bjNudzBkLzFmSGo0ZkFwSitNQnFNQjdlUVdjZUZTeWpqdE5jTndWamF4V2JxUkNUdisyTHlhTDFRZklJWTF5RnJmcjdZMW9hU0N1NzRUcjdOTENLYnZ6czQ3QlhHZWk4Tkk2N0x0cG1xUkRNbUNrZDdDbnN4TkQycmlNSmtKazF1emZrVFdnQmhDZEV1WnpKZ3kyZzg1OHV0WlROclFRWllHRTExMkF1RFlvK2Rnb01lUklEYkJ2SU9Md0FRTFQrU3JrVVBORjJMUzZrOUMzNGlpd3pwbUtiL05oQ2JieVk0WUZPRkhTWnBKa0l6NUJOeUhXd2JLVldYU0lRMUNOOXlHTmMxWXZEa3RsWmcrODNvZ0E0Ull1YzI0Q29KOVovQkx0d25yUnhuOUJKVWU2ZkZnRmZPQjR3OU1BcStXOTBMMHJRY2kyOUVVRklITGFEdDVpUDhaVDlwSmU5UXpLVGIvQTg4U05zaXpBRU9MdWxZMVRzZWlVdGNIZy9HMTc4dThQdFNCK1F1ZGdtd3Voa21zbVdPcWxZZFpXemlQYTFhekRxQUJuNXdWaWhNcGdQbkFjUENIV0hhUE1HeDU1ZUN4SVlXNFQyemwrSjdLQlV0MnNQWkpnUE5HeDdqd3JmcjV4YWVSWCtPSmZpSzhqdEp0YTFrUkVOVERFeUNQRHp2ejJ5VHY3SysvNGVEZUpiSlUvbFRhQkg2ZldXWmx1VkVaRkIwZVJMQkpFZ3JwQVg0bWFxK2M3b0toL0kxeU9uV3BMOWVSN2N3elNqcWZVZzR1MHUwcmhBTEcrZXVOWG93RmVLaFF0MzI2VDYwQ2hRTGI4TFV3dm5hZzd1bkp0YnRkWjhEOVkwR1lVbytjaGhSbTZaL3lSQm9BNFZsWUNNWnBiaEg4TEJqQnh6QUNwN1R0d0xyMkNQZzVnbDREaURvQzlpVFZIbWlFUTFpaFcwUFNmQ2MydE15RFRESXB4U3BkakorZC9LcDdObzErNGZ1NXJnNVhwdUxadDY4NFpJL1RqQ3lLMDBZYWRkaWNkUlQ5UitDbkZDNTcxU1JycUw1V296Z0UzYzdYd2RhcHZTUFM3NGd4a1cvNlM4UkI4Vmx5cStTQUlqMFdHam9YdHdsSTR4UEtTSkFSaXN2ZkU4b1loNHI4SklIdXZna0RTMCtSbUV3WlhBNTR6cXZRYVFUM2ozOFkrWVhtbWR3YTY0dDZnaUlYb3c0c2J4aXZZdHdTL0NieWp2VTZEb2pSdE96Q0JIU2FsclRZSXFDbmpWMkRHZGs2WTVFU0VZSzVnRWEzdy9ua3Fvc0xuOGxKUm9QWkRaLzcxcmVQSHBSTGc3UjFRT294cUZjbHZaSloyTVZWYUxMK3N0dlBIQnZseEdzYnNxRENSQk9DRVhoenRPVllmbnpudGEraDdXdTloQzZBK2VqaHFuNjNvc0ZKVllBRTA0dEI4K3Y2S1M4S05lNC9FamtwZ0diay85dzZEN2IrVEdQRHozdE9wSGU5aFRxM1NMNXZZeUNBU0hHSjdoelNMMm5lUGNTbTZwczNpd3FDM210UHRxaEdhQTIxM05MYVpycXRTVmJXVnJVWGRnUkR0OW9VZWQ2Tmx6ajRQSUI3bnNFOWVOSEtSeUxpaVFmYTRrWVV3bXN3Yk9zUWRoOUhzaXYzNmVKL1Q4MVhMQmR1a0RQZ1JLQXNNUHQwSndzSTcvQS9MbGFlenZUbnBqYS8ybFlOUXEySnBRTi9zdWR2emthYkdkSWk0QTRWRWJ3SkZWZFVZU1JDc3ZET2g1QTNSYVh3bmpBTkJEVzdzYlJ5MnNtYTkzSlhaaE9JeTUwVXBrV2dlSGNVNWFiNkZnN1Rhc2VjbHZVMFhtOXlIcFdoZmxVUHUvMXJodnZydHdaMHRsMzVndkJEdjhCczJPUTZxOUlrbms5MVpGc2ZuMVU5ME1HZ2N4SDc5VGp1ZHlmUUpRYzFMbmx2UVo2eDE5MnBjbVgzUVJNeDMyd2N6RnJSVnVydzF6bFdJUUFQL1laMjV5U1VWN2RDSTFibE1MQitVVWNqUEkzdWQ3Nkt5VGxvTDU3WDVGcVQvbitQVGRWTTBhSGoxMHdzbGJDTmUxbmNSemdIOVZGTjMzT1p6ZjgyMDRMamp5Q0xKYk4zeXFmV3cwMWxteEZTemg1bGNvb29WMUt0dXlaaHMya29UenpESmRHWVVCN3IyTTJVaGxGZ2pwcjhhNlQ0YVVCL2JmNUZoQnJHWS9pYmNlOVpnRm03aGo1ZjMzVEgrcCtkQVpZUmtMclU3YUYvRVBpakR2ZTIrVXRuRjEyQjd2MzNLUVFDVXh4ZlduK05HZXRhUDl3STg4TlFTcUhnNWJ1eVErVkVQbGQ3YmsweTY0SnBUcHJWK3FrRnhWQW1DRTMweFViN1dpVU4vQmhCZ1IvNno2V3NVek1mUG8vQmJxbWViQUdiQS9wZnFwaEpzMmdzYldtVjVPRnMwSEJtalpRZERpK0NVZGNjNUM0djNNZE1vNFFRZHpiUnlnZWo2V2JSaUlTZU5FV3h6QVg5YndKNjVtWmMxNjhJZ1o5Mi9mVHhqT2x5Y3FnM0JsbXpnUExyVjVDcmpubUYybkZKd204RG1WTEZ1cEZ2ZXhaRXlYRFAzUnVDZW5rdFVWNmdRVkVsdFIybkZqQUVuWDN0ZkhrTVNIbmJSQ1pNT1J1Mm1ML2d0aS80T2I0a2xOMURpWk5iekdHdVI4YW80enlLMjBPUXVuaXFnaXVpbFloRWhEUFdMVTMzcWZwdFMrRVJMMGlnYXYzcWhSTEhYZGo2YjBtenJ0R1JCc3ozc082b0tLUGxxc1dHVWlUMExnME1mV2l3MXM0NFo5SjcwRll5cGpoblFNblM4eHBxRlRJc0Z5aDNKTGdwMUNhdE15dkNGaXpvNlF0b2tXMFR1VVM2aXk2VGduYkRTS3o5TzR4TFk1RmU1WUcxZUErRXVpbitrdFExMk1Ic242UVBuckVEcFAwTWdtS1Z6YStlaXhOU2o3L3B4a3Bnd0srMWF0NzZTWmFjZ0RrdCtEMnE0Mmc0QU95dFg1dkoyNGx5NDcyaFhqaU85Sjl5N0RnQlpqYmFYWncrdmovMWc2QS9wTHRPZlFZRmkrNGRCNEZRV2FvWWVYYmFZRERJWU92R1VyUW8yaEhsYm9mKzY1KzZvdW81VnhMOFZuWHprU2RiU0FLbTlXenZkL1ZxU0haQzFabGF4bWVtUHFzZGl1Ni9sSktibFA1U2JFR0RvT0Z6cnlYYmYydFpoZ293NnpDMXMzemlTblpvWExoNC91TVY0NmJHUVBhYzVOSmJvd2MyWnZ5OHZKVzk1b0xKRU4vSWZLSmhLOHY5eGc4N2FMbktBUnZTTVNyZTM0bXBLVFJ2eDR4R1dQcW1Tb3AyZkF2aGZLQXkzVHJLOVdwWkU3MERVRnJER0Z5aEhyUDB5Q25PSVBweHJYVTBDR2oxUXY5aFdsbGhJbHdQbTZKK0lnd1BwQS9lbnhWY0xsd0tSVGkvUzVHZWYyYmFMWVVESmk3bU9nNWh4S2tWZ0FjVUVYQ2duYkRiTkN6eTI3aER0VmJkKzVBMEdaZ3pvRlVOVU5vY3lvM1pIRFBFTGNkakVYaHBCZkJOeTRoWEw1RndNQVNKRkxIUTBtVkg3eW5VNlhJUk12OTJrQlBGWmZ3QnRrcHBKSmUxaHdGTWRWZC9rMFRsM1VFSWJHd0ZKUUhyakdKRVNQWGhLK1lrWksxb3p0dEhIbTE0LzQrSUh4RXJ2NjlXdytiNkhIcHlTZzBCSFFnVmpBcG9sanQrcFU0ekVFOTNRdDdmS25wOTJ5QTJlWFNiRUVGQXpHMFBmbGxaNVIwSFVLS2RNbVJ0N3p6M3p0ME1JTDdsQ0dnQ0hlcitjc3JVNmpHTnpBR0FCS1IyMUdtc0dFRHN3V0JJZ09zaUF4OE83UFJrREpNTFFONzlRWGxpbWZMbVh6WEZRclpVekFlRGVVS0t0UHhQeWNwNmZUS2wxNkdYTCtORytuZktBeGFlRTV0Q2ZXc2RjQkJzS2RBdTJYVTRKYW0vY1BDbG0zYnhLSS9XbE9UNys3RDdScFZZNHY2bGNrQVk5L2pHZUhtYU9TYlU1aXhiczg2eG5JYWxScnplcmhkSjY3dGRCTGdWUWRYalpaOGxMcUJaRjZwZVZ0TG01dk5MSENaVEJSQnFNNzhMNjVPOXlycW1OM3JZbWYyMWV5WnVjYTFYTkQ0NW11VXkyczBpb2J4ZllKaU5xTlMyVnBFV1FTdFlic2NHYTZKcUVSVXRRSEMrUVU2WUNYM2hCdms4aEprYmM1S1B0UUpOWkFuTW9yaGxlakVUVUx0VDRNd2Z6RHhKRVhIRTE2MmtOSEVxbHhiVjVTMjNEM1lzbHV0T1V0Z09FQ1RSdWlBMnVWSFJaK1VqMmI3b09jQzVFMDQvYUJacDNvcnQ1UENiTzFpY0QrS3VaN0R6U1dUR1ErMnNoTytpOEFXQjZDMGh1a0pzSHl3MzJOcFZ3cWlwZU52YkkrMGdlMlJVMHF5aHhoK3podG5ENVVybGZwSGQyeXRQUDF1OFNpenRwNzVzZmYwUkhPeXcxdUZ0MHNyVGxwRUlyb2c1NUtUNGd5ZnlsOG1YRmVHc1JBa1piSkl0Q1Awa21UWU1YaGRQRkhVdDR3cU5NUGpwMnkweEk1UGRKMHhXazQ4SHNndTg4QVNBVXVpeFE5RXp2U1oxUUh6dW96V2V5ZEtZMG1FMmVEZXIzZ3RlRHhXYUtKQlBvNS8zNmE1NXgwalBiVDI0b2xRdnptNldsbExtZ1NpQ0NXdWxwelQ2ZVlIWThrR0pGenZRdXFCY1ZSSGorMDhQMnppYXRyNmdRRnlmRnYyN0JFVTYzNEJWRDhCTS9XaERmclIxRzJlR2NiYlZ1TjByQ1RicU0vR2RnRDl0NXJwZTF1bkozRmhXNDFJRGRzd3FFM0NaYnJLKzdDTU42aDBMajgyR1NDWDBiUXlENjBDc0t4UjVMZmN2TkxHTXJXbnpuQmV2SGIzLzJ1RVkyZzJadmNIZGhaL2FSZjZmWktMeGVjNHJvcnhpOTVDZGdCNFdUbnpCR20raXd5ZHBUSHFESGI1Yk1iT1hKZXg1MXdta3Q2eFI3cXNuK0R5dmtUSW5YNnZ6N3ZXdGNTZVp1YUl3dlIvYmY0UmxDYmtCZWJMRk16MlJSdUx1TjUzQmVxZ3hRUG5HbXBmcENHaURkRUxrdzhCRlNZQTAwbUpJdWNOSm40cnRVRkNTYmx3VURRYlBpZXNxUUFCNnlnMnowVFRqZThwWUYzc0JCVFYwK2Y2Zyt3UVdHVC9oQVFickhvb3NvSFZObUMxdkV0VHk4K01xZ1E4QjVyclhld2dQMHFkYk5WZ3AydEJYMVJOYkZjYk9SOFdmS0hnU0Nyb3k4b2txQlVoay9ZcVhtN3Nya3FER1NZdXJWVEVVbDdPUEkvZndqeDR6ZmEvT2FGVU1EM1E2eTlPbHAraU4wWncya3NseHRBOTFndXJsN1dKYThGTTZ4VFVLMHA2S05sUm9KaG1ZaTc5MGtiRHZzM2VDby80SEhtUmlkUGREYkpDRFpVTU1pY1A0RDl4Z2lVUXNaT1hzOHFqTGRjYXM2VHZVQ2E3L0dQMk5JL0RNTUIvQUZkR09IVVFjRHc0SXBTT240VzZJWHJpMmM3dTlhbitEaHlGLzRDRGtTTk9uVUo2a2V2TVhoU1VIOXB4eDFEUFphUDhRcmJNNUZuVDlIVVl1UXZVNHVhdS8yejY5WXNLenZ0VndCNzNFNnhTeFI5RmNDN3dEd2hNSXRCTXBkZ3MvdFJPc3hqcFp3MlJCT3UzY1FQMmF3UVphVG9zaFVNMmNYd2hlUXRUejJ0aXh0bjFZMXNETGZJR1NmVThyVm9OZDBRNU9Lbm95cHF1cytOZTA2YUxqS2YrcFVDbFNYcjlFRnkraWtZYnpaRlJsMjZUNDNkWmJVc2owZVJyak9EeCtxVGsrWTBaNGtzUGRROVNjVS9BRUo5QUVTWnNqWWpQVks2dUFwUGtmU1pjRDFyMTVKWjV3WmdZbjlWOUtwUEZrRVhrbmtRT2hmREVmUzZWc0tGa3FzN3JjaXArN2FkNUxXM282UjBvckNQS2ZsTmhWZUN0QnpoN2lrc25mb3JmWVNDMEVwWXpQVHFNK0tURW5ZSG9FMVhPWitnLzJwamxLei9aZzRYYy9DTHUzdUFCMVZ6YXl5dDB0UkFacy9RbFJSNm5lQ1oyb2VuanF0OTZ1cWhwM0R4a01hMFJmbUt5ckVScGxwcUd4OVBIMm1RUVpiUEhFaE9DUXl5VVM4dzlGZWxjTVNUcmhGS1VYT0ZienF0QWpJaDM1MTVGelNPcE85OE96WWlXQnBzUlZFUUpqUitPWXRUTU8zaU16Sy9YZEY0c2svY2pQdU9RUVJUYzhNcDBEMHo4aVl1YStQWlF3Q1psUDROcTh3Y2VYdzladzlHNURNaG02bDV4T1FxZWk1WmVzQ2NJR0RYa1ppWUJNbE1EVk5PbFZhNjNYbzlwZXhFYTNsN0tlZVZpYVBMcXdlbStQcVJYdXdIVllPQzVVL1F2dTNLcUx4cTk1b0pBUzdpNmx0MDh5L1BGd0hnZW5HazRmRWpUeFVZWUR6TnU2VkdzSC9uMU9YTkc1MmdDSWc3NkxTTXRWQVBpVWRvSXZrMXNhT3RvYXBGVkFXa2xBWjQzMlFJQy9mVThicHpiN2hqZTZuMlRxUXJMNHZ6NkIwaSthZDJRbDVoMDloU0UwQ0tta0FJSFJCN1lKdVFVaWY3U2ZMNXVhL3I4UDhJeVlyUnI5TVAreS9aQmZKeDVsc29FcE9odFJJVnY4c3Jjb0lZRXRDa0J5d0srNUFBMHRaanVCalpXNU01bm82YkJoOTFnczUwZUJyRE9oRjJRcmZvTFJrNXJzYmJJSmlOUkFuOWt5VTdiYkc4NTlDWDFRR3MwU1VlOFpNWS91SjVQSjBFd2JFTU43aWMyTDBZaVpGdGlFVlJwaHEva1ZOL3RDdUdjTDg3UXNGejZHV1RNbkszVXl4LzdQM1BOUVRuSlVPVVc2RzVaN3Nkb1lVUityMXBKN0xxY0NDYW1OYnFUMHFlem4rUnFyZTNUcnRWMEswbHZ2aGxJdzRJVGR6ZEpRUmpXWCtoWUFMb0RJbUM0QjMvMFlNRXdUbHVLbER5a1BEQ05hNmRCVjkvUnZMQnpOeXRLTmJMOEdjTUJnam1rWTQ5dEk3NW4xNkp6UlRkVHhhQVRlQXZpMTY2aXZacWJuOTBPdVZ1QTQzTE43OGNDaWxGWG9VYmVySjRXQ0gwTjlZbk9KUFJ1dW5TYjRpWmRkcHVNelRmVVVPd3N0bVRWY3llYklKSzhSZk5MZ3RnRjRmc2IyOSt5djA0WjU4NDFRdDgwd1lvNjdDdUdjKzl4L1Y4L2dxMGllOHg4bnV2c1FHVkRTaUFwM3llYitISXRyVlNRdnZoSXFYZ1RvYnlvVURNQ3RtMlBydVgwNVlJSGJiRHRnNmtuSk1XQ01LOUIxa1pWcVpYZi9mRVl2T2dBcmxleEQ4NjlEbFJxYS9ZK29YVnNnbEF4M0U5MTVqcW81SDdQaHA1S0o5Tmlhb0piNjRMZTBnL2hlT2F4b0ZSejlHSWp1bHZ5UDlTdHlIU1JtdWdwa2VKc3NENk1tVnprVmtqNVJ1YXdQdStPSG9samVJb0xtTnphUDYxaGxMdWcvajV0ZUVJQnErMUpkNTFtWnRGQVVLajVEdjhXUmFndklEVnFOV2hNMkxRL1ZTZnVUb1pBR2IyY21JVXlOMXp6OXYwNzVxUVQ3V1c2NnVGOW8zOVNVUDA0Rmk0SXowS21GR2RRRGFUK2xVTVpVaFoyaGorVmRscDNPY09jUkU4bHhEWllzbCtmM0RhcU5JNER2OWpPVnZhbVJaK2V0YlZrbmhyRnlNaGhlZUIrY2tyWUJaTHZzNEZXWW41dnZ6NXRSVTJoQkRjSFVaamRNYXZ3QmlZSkZQeGtyaXhpYnRPSFZMaVMrTjM1NWtsVm9ub0pSL1RSZUsxM0taLzlSNnBnOGZMblZGNm1tN2M2VWRRaXEzbnR1TlV0WTZiTXk5enRqRTluM2k3UHNPUlEybnA2ZFhoRHo1VEMvL3ZYWFF2b092UnMvOHR0T0M2VFo3OXhrZDdpeU94eE9VRTg1NFhQZEFIVkFxYVF3d3lmTlNoTWxTSUxjcWZvQW9VaVplQVJUSW9ObFpjcjU5N0tHcDRoTkZ1TlJwQ1o2L0ZXOVRVWUs0M1pBUU9iUlZpd09sNHMyKzA4MURsZGhEVUFReE4vVExQKy9LK25Mb1U3WEFhMHdPSlBzZCtNR0RBeWxZZDFhampQbVNoRXVZZm02cWl6amNJdTBIQTlRUW50ZVZGaWl2amJPYW5OTC9RRTJTekNPdkgxOEJHVWwxMjR6cnBYRWlKdkYvUFZPdmZOVnpLNksrZXdCcGtlRFZzMy84b1g3Y2k3aVByTWF0T2lWdVVPU3RXcVVURnpQR2VRL3p1dEhaWElTbnJGbEllb09hWitNVVBUbDVQL3VSWGQvdkZ1eHF3OWhwMVdXNlZPSUxDeGs4L1BYV3dDMlZhVWQvV3dRa0JjNmI2NzRTNmxlcVVLaFAvdXJUNERFZ1k5ZUU5dytIZGdtRno1dlFqdWNCL1M2OUtDM1RpMHJPOHc4bHlma3NMd2xDcGlCR29oVm5lQ1duZG5xNm5OZVJ3TDJySmlEZnVCTG80TTVaKytiWnNueTJLSnN1N0NDenJiMHBNaTJ4eUIzZ01aRjBJYnVzMnprS1pVM01TTEdyajF1REhyT0txUTlKaHNwdFNiUkhXSWZkSGFDem5yRXpWY3R1cTNVd2p2ajN0NTVzQ1hJazhGaUpqN3AyQzBGcUZrd3lHalBPYncxaWd3UXJaZmI3MUprQXgyZGJueW9SL0RMbmo3d0xFZW1ieWVZUnhrQU1WTUw1MFlPalJNWWVGeURWVTBodjFUYjJXclQySVpzdHdTb2g0T0VXRjVoaDl6S25pVmYyU0R2Vjg4MmVPOUVhUnVYZ1lHREpLNVhSK25TR2JOYy9PaGJkWXBJLzcvelhwVmRmV3FHekdTSlBuQmNsOEtMNjh4S2ZYb0Zjd3NLVnhhaTlaMjdDY0l3TVBwcE5aamVreGpjSnVHMXRBQ200TkdOV1I1bnBqNE1QTkRGV3FKZkl4SDJPUFQ4eFFmZFpMRS90YkcrVDhZMG5ZdUk4NzFFeVU4ODBrQy9hN3pHYThLRWhRVU5GYjZBcjJndzQyUnBxN2xzbUt5UGYzSXNkWFVFNnV0ZGVCME1JeUEvN1V3Ni9sMUZ4aXQzM2hqOFhxRGVNMCs2K1ZpbFpYc2V4RkxLaEtibGNLRXJYUFhENy9UOXdNd010L2pZN0N6SEQyYWdVQmtudGR3YkxDaG95NE1mbnJLWndxSDkwNUlUemJJRFpzQ2lmNlhzcmw3c1JselEyNGhLYWtrRXYzd213YXVpYjNCdlB6UWNmMHpONDJBeTdpOWYzcDhoeWdMQ21lYm1qRlMxeVVuaml4MnpTaWF0ZnRiQkxiTWIxWU80eXVGSmJ3UTg4bHE1TnJMVnJiRkIveE9MRk1JK3cwUGFPSGRCVVBVVkpCNTN5SjlxMG1Cd2FTRDhKY2x6d0RsakQvZHhUTVZ3ak95YXNKQVVuQ28vTHpTQzRka3ppamhJa1k1MUFaTGNOakFCbXV3ZTV6eEduM0xiRm9veFFJazdSOTNMWWRWOFcrZ3g5OWdxcjJqMDBTN1pyWFJOZWcyczdNTEdrTnA1bFBpRkZUVVZ2THNpMGJMbCs4b1VNQk1iSnRhSnZyUjZrOTNGMEFZQS8rWUg3cE5oNUJwemxBNFN4UzBXLzl0c25ZUnlKbkVtZnZLUkowTlpNQ2g4V1k1WlpOOVB5OW9ObkVoclFOTmt4NXkxbll3ek1QaHdZYTZpZzRrQTF0L1Z3bko4a0dINDAvRm8vVk8wR3NsMHA0eWFVOW56ZDFxTGhpRHdCR2ZrRGlMSnBleXM4NEw0SmVmYmtBZUJSQVk3UlBLTXh3TSs0Z1RwWUM3Y3pNK2ZET3NyTVFVM2h1OGF2Wm0wdkNhSEtwakU2eGVIMWJ1c3NsZ0lYUDlDWURoT3dLNEUwL0NRNXJ5bzZmQ2JyQmJpeUxNQ0FkeVd6dzM4Q2xqY0RzNWdFdWFFUVhFVG1vN3pKbzFmSFNwTjFkOWFvcHhvM3RjUjZ4NTkveFBUQmV2WkF4UGVUcHo4aWl0QmNVUDJad0FaRSswWVB1OURsZXYrOUJObC9paWZBajA0bkNSZFFSa002YnV5K0VGS1k1Nms4ZTFFVHpPR3hHOURzZmRmY1Rla2lKY0prQUV5bTJDS3Fscy95RTNZZDJWWGp0ZERWd2NPTE1FMS9ELzVQaFZJNzJTUEcwMmp1YUVHUndMMmxwUzgyVS9Gem0yblNEdno0SlU5OUFCSWdXZ0J4K3BxQnZUdXZJa1k0NnJhRkZTL0R1KzFYeE1XdTNVVFpsYU95TGxWNWs3SXEzb0lNVDVBamFWSGdjTVE4cThOMzJiaVFWNUxpRFhGSG1OVlpBVVlkWE1IWjRhMGhsNU8vRFhMYmpwU0JwalF2Ty95MlBIcGpTVzBvWVYrQzltZEZ2MVZNcElNckMvbGxnM01RK0h4MmdzaWhnVnRMQkZ3Y0kxZStibExlN09hblljVHV3UjlUU1pIZFlIRDlSR0FWYXRCRDhTL0NIZ0R1bzY0Tm1QOEQyZTBHbEkxUW5uRlJxTHlVeXdnU2sxbjZtNWN5SFlTQlJ3bFZjeXJ1ZHdiVS84Q29wTENlbVVaQ290dFIwNFhVcjVLeW1IT0pIL2VSZHVRcStlbFVKOHY2NUh0UE93MGJwbEJmWmw2MjRIRnFmSlBTSFpldmhEbzdNR1ZCU3NBN3hORmJ3M1cycGxpSjJ4YisrdkZVdUgvcVRIWkthN0NneFVnRG1lbjZhSG5NVTk2QjBONUN3WXIyY2F0bGYrUFFpS3UzMW1FSDNnbUhIUmR3VFJ4cGFZVUFjMElxZDhQMTBDeEpHSVlNZFJRcjlGTkNsZlFMM3pwTCtBVTdCT28xVHJxTVlOSWVLV2xrT1lXVVNKZjNkVlRiclluT05SSTRXVGRyenhreS9ndDVkU2FMR1psQ1FGRnhLRG9Sa1VmTUJTQUN6RTNtbFgrOFhsY2tLaDRhcTV5VWlUZjlMSExCK21kamk4ZGdKb2V4QUZxckdaS2VVRGlZaFpWQ1hrb1RiaWFGR3BFbFpzS2dNMjhadkFhMDdEbnFhL3pvSHV3WFJwV09sQkpHcHJkNm9Wa2MzRnRnVFJHQW41Skhqa0xrRjc2aDJuaVZPeE9vYmRsMHJUSndoOFlvTnR3RDZIeitTbjMxcUJIcC9nNGV6ZmhMc3QxSDhJRk9SKzVXcHVNa0V5emYrMHJNalFNak96dnpUYjM4WThiNXpxUkxVMGdhRjMxVWtZMUhlN2hqQkxGeTQvL0ZwRHFNZUY3emdXR29sTno2eTVWQkt4aG85SjI0SDczTnJibjlxd0FkTVZQdjBZZVY5QkxJYTRFVm9ucDVzZzNyTGZkZERyRnlGaEo2bXFuM0xXekFQalVaOFNibzJDOWEyQ0FyMEdmdUxlMTBKdVRTUGdqQnVPTm9kL0ZlcCtjSUtZR2djZWNXKy83bjY5amtIaDFRbkhBd2QyVS9YMW8zSi81N2lMSW84UEVyNnNxMThDZFBzUXNOcC9XTEVZNGpjU3NBajAxKzV4UHkyQ3psaFdSTko0UktMcEcyR3FzVmwyUlBqSlJZeGo4Ui9kUllMUXQ4WFhjcFNyL3U4ZFhJZzQ4MU5PSUJVZ1Vkb1h0NVFpMmFGaGMvVUhMVlhYZk04MWs0MXBVd3FWYzhSU3ljK09nOXhhSE9pMkJna0U5WDB0ZVM1TzY0d1FnZ0hlalUrR3VUOVZacnJsODNqbXlGdWNwN0w1aFRLeFJpRUVMQnhpeGUxVHdJa2ZYbnVxbFdZaS8xZWZ1dzlQUXR4cWRwZmJMdUtqWkNNNHkySkgrak0ycVYxNlVUK1puSlE2ckRhaFdnTFBXYUlFL09JUWo4ODJXK2IrSlJZaHh3ZXdiNmxIWGVzQmwwekRnUFdGU280TXRhdTJvOUdqQkFndUxmWWduYU9hRHltY3NtZThuTkQ1Q0xNdVRFR2FhQkRkcnk2c0NHSStPWXR0aUhOWVN0b3FzOFU5Q2I5UmVZOG5QM3RnSVRRM2VQVDBkTlhjOERmZm5JZWg5RldsSVhNOGw3djlzKy8zdVRwcER3elV1OUNza3Q0bmtsbW14c0gwUHBYSnNyK2ZIYnQ3d1ZrZ21ybVJDMG1Zb1BFYkRBbFQwN1U2emVLSFJ5aENmMjhGVVJuRzIyWUFyR3FrcmgrVVFjTFpwQWJFZzd3UmhjSDBJOTVwbGE2RG1TbGlZMHRKM2hxcVRZNWhWN09LakdLRHRXQUZBSjYydHY3UGV6S3NqQlZ1dmhGUEVLMFpEOFdFMkhqeE9PRDRCRkZkZjBXbzN0L3hTVXZQY2VMYUt1UjExQ1RyalpmV1ozOGZiNUZWdWxFVmxLRmxENmQ4OHhwN1hwdkp4d2IvWWpRelQwTFlSVThxbU91RkVEZlR5K1FNNG1vdjJab0VYR0lXUEVXMEUyWXA4MUtOMDh2QTREb2QvREpreENwdnB6TkJ1Q0VvKzhpMkQwOG1zZHI4WDFzWkVYUGd4NzNkeFZTY1o1dkg2LzF6UUdKWjBBZGJmRlAwSFgwekVhUzUvSmZxcndpZmM0UWFhaktIdzVKNEZObFZKSkhkWWF5bE5vbFJFelU1UDdFVjFkUEozbVYxU0gzbEsxYXJlTDNHdmhTQVg1L1FtMkNrbzRHUU1KbHpzTE9zL2JJUm5PS2RKNmRybXZqUzdBQWVINU96eXBUWXlHdXVNWFF0dzkxUGMxUktscW82ekVPaUJKQXFoK0lwMFhUMTF0RmtmY01NNE5kNWg0YjJwcmgrUWtnUTFHVHdIUWdnQlNIYWt2MUY1eGhOT1BYWXFtcW1nR3RvaEFtRlpVOC8vQXRHWWxQa1h6cFhtSE1DNWNuWXVtT2tWK0E4cFljWXVybkYwWG91ZzRNZFNmVHliQTVyZlVOc3RRNURJWldEMWNuRm90REFOY3o3Z0M0Ti81UUZWTHlXY2p2aExjOVJWRUtiWG9sbXNLalZFUDBOZGpPblRTdWhCaEpndDQrR0JIUkVTMlE3MTV2OHN0TnJxQ2ZaNTNmRzlpRGR5RWVmYkNjOTNycFBHYTZoZHYrVk1scmFOYkhaclhkMmpML3VnRDQrZ3ZleEQwTWI3SWlQajluVXRKU3p2S0RRdlp2RFZaM1dvWGJzKzlJU2FCYlFpdG5RZnJJc25YY3JVcjVJN2tuQ05JWFdCMXdEenZ4YlpKNUUrOFhZV1ZQbXB5UTlnM0pKQkcvUVVSNmNLUWRpRkR4UEZSN2pLOWRaS0FHS1NoQzV3bGZnWlVzT2VCbW01b3kwOWtBZHlJSEFiVGhYNjNJMWFTdStlK0d0bUtkbkdyeGZ6SUc4V0Vmc0xSaXB3ZGpSWkdIMGxJVm9wM1JucHlOUUZrNHlkWmdwMDFHTFZWUmc4T3EzdGNXaUxiaTZVWCtSaVlHNUpuZTVmMU9YOC9BVEdFeExzS3V2K3JJS1VCeklGMWJjaUNhSStGRXpzUDBnK1hZQmtEbmk2YkZwR0x3T3g0Y1lRNjVvdTNzUi9zeUpWMTdPN0hReklyYUxpU2Fnclg4TE5Tc3ZpWGlDaDRuVjkrKzR1TzJDL3hKS0E3S1Z2UktlNHVuZWlFK0dnSEpiVWMvbXU2ZVo1UVdzZmswdXJLMTUwZ1BrZFppUXRaT0FMU1JocGFqeDdaWWFMZE1sRk1tZkJRTjVYN2h5Q2FNK2lRMnZJNFExZ29qN2gzUk9hdzFrd2xwVFlXTFFtTi9ETzZOdDZEOW5FVHI2WHBaR2tORHBaNzdQQzQ2SUhXV1l3bHc2aEZnc25mSmt4bGFGRzBjMGd5ZlRQOTJicFkydjFiSGJKaHhaTEZ2U1l6TnpoMGxGS09QQ2l6US8wN0RtdVNaSDhsd2dUckNmcVpBMTUwR0pBdHdnL2JkSnk2anZYbnRtbGJLeEp1emFPOTc0UXZBL1NkVDA5b1ZKM2NOdStIWC96VTFXSkdFckxHL1RZM3p3bHJzU2pYWDl5b241dGNGNHlQc0FheExiRGREdWJ6b2NGUW91OWl4WktVY0R2TUErbGIweTRHODBkVEU5b0x5a1J5RHhveFBVaXZvcCsyWjlsa0tlVGZ4bU1ZdXBsa3h6MysvRHhVWmhLOFZTU3Y0YWdTTXR4WFJhK2VRV0pVY0JGWXNOekFvWXQ5WjB5Umd1Uk03c3QxTy9RRFA3TVV5WEV4NWVYUzA2b0F6Z2k0ajFiTGd3QzcwSGJseGw2NDJIaUorT1BUaXh2U0N4dElwTDhGSDVSTVVRcnA3YzBZa1JOWTFLMnlDWkhBVTA4OStMcTFPdXRiaWlOV255Q0tIblhqMk9sU3VRbks2VklxNmw4R3dmWU9xd200WUtFTEtsaHNIeEJXWDMxVUF1ZEVTNDR1K1VqKy9PaW5Qa1phQytKMXVvUEhoZ2F5dEhJdU1jMFZ6dEhNKzEzTWsxRjJNSi9wVTU1QURZK2lLeVptaTFOTlF1Tnpua3RXcnI5VXgzWkZmalNFOTFXZTRGSmluUXRrY0xaMVRnaXVnU1RMdTl6T3Vtc1hvQUtKNEFIdnVsWGV5YU5oV0h3SnNxQnQrdWI2M3NBUUtXaEp5a2JsdW9zclhTUDZOYlFFRkpRN1ZETGliWGQ3SjAxZnVTTDM1UzQvS0Q4NGVzdXVZUlQ5bFFCZXlEa3N6cVhyRjFleHpxa1kwR09iajZBQzJEUkJncDVFNnJicENSbUprYm5iS090THFVSFMwdFIxR3ZyL1MwVnU5bGViUGlCeWRteXU0N1Q3cnY2VzBDN1BFTzc1eHl3ZFE2ZTFoak5aQzcxM0lIdHM2bEpmeGdDa2ZmSjF2OU5YVTlsNGY3VUk0ZjRnYTlhcHdYZzhxYnkzSWEzZmE1Y3NlYjNLL3lGM0ZFVUd2SXBCQ3cvOFJYVER5RldxZTRTd0lBek50Umc4NGtBNTVjNDNGTzVrSmtiWm1HTS93QkdyRjBUaER6Z1dVeFBMYlA3dk5YS2dYUGU0djhUOXJUOUQyRjFiZHhtOVlHbXNUK0p4YVVyQWhVdndvbUMwSFQvdzAxeVpra2tXdCtYMkNaZE5GbFcwdjNXeVpzZzVOYzVteHdWcmV0dVVkeW92eTFIMlBiamZQdnJmTkpwSnlFdGhmN0UyV01aUTg5V01HZHdibHo3VzRBZ0xHazJDOVJRSmVTek5wNWZKZUFvUURQOWEwZTdBYmMrQlJnMEh0azZoYXc4ZlpPRzZsRm5UdEIrM3ZNTHowWFdWVEU1cDEwVGxmRm42akpxZlJOKzdDU09CaXFmUU9tWVVPQ1BNdnJTejNrUmZrd25DYVVMT2pGa1ZZVUVLMkVmbFRCVzlzSEs4dm5uVlZkR1VFcG9qT1pyOWJQSTFRNlM1dEhqQmU0UGJYUGNHZ2VCRGhNcTJXNTZMVXR5bGQzYzdCam9md3luWjBDM1hXeEdveHZvSThZVllGSnJGSnAzQkxXaHU0UlBOTjNCRnJwbHFoSFN5UnBLT2J5Yks3b28xSGhCamF2M1NlbTVwNzVXSC9qc0N0N1dVelcxZWNMM2ZRSnp4QTd1eG5kK0M1RDk1NnZSb2NWWDgyNmRobUt5VGtiRTFRVG5RTmlSV01URGdjbWhveWtnWC9MTkpVR1RieDJvbU0rT2xlOGcyQzRsQ3NaNHlURTFrM3ZNUWYvK3BCeW9odW50My9mdnZKcE4yMGpiZ2QxeHExWU9IS2hYVDVadnpMQzZjSXBnbWJSUjhPQTNZT0RaTjloTjFtUEdTZE5CeVJPZ2lBS3QzMlJVYXVEcTg5YXprZTBsNndPZ05kM1RGaUtrb1d4aGxQZWVXcXo0VmVrQkFDL3NkQVFMcWJxSlNEd0VhdkEyanFuZFZQZHMwZ2dhd0kwZU5nMFpVUTVwdTBEZ21pU2N2dHc1QXV3RE04amlIWkEyd0x2cnNzZGFZVnVpVU1td1dxdkNOK0ZQbFBqN3V4R2NuWURhVTg4TjRpYzRiclAyNEczMjRRdmhNSXFsWGhyb29OOFlTeXZRYzRNNmVOOFRlNE8remJPR0NiVlBTejJjOGwxa1I3M3FFeXRpQkg3eWY0cXBYQ3FrbkJGS1BxaGF6UXpyQ2NRQXVwYWp3NEl2eE4yZWxja1A0RTNPM2M4Ri9rYWdMd0hrYjlXRXphQXVmR2RaNDMxQjBDcVNjMXNxY21saytBcW8rYTUyUHMxa0tZNVBUS2pKZWM2YWJISnhZdDNzSS9yVTZpUzdLNzg2VG1uMmhCVUQvUFkrS3hMK0wzUmJXTDNrTjFDN1Bob0Zyelhzb2JIdWt2N2J4OTU2bVlpQlY3WDVmRVZqTlIxTDBXd2ZUUXFhdVQxakFtSElDNmhFdzY4bTVQUmhjRzBmakt2SnFlRjY4dCtUeUFaR082RkpmdW5yU29GNmN3cFI1b0lIQ0ZlbHJoYi9HSFFUUEo1Y2lLRzBNK0FSUEpDOUhRUDI1bUFWNVpWTGJaNm1WcDNWN3M4M0NDS0JWU0QxRXBYSzVIUkVNTW5QSjVRSGxVdFhYWm9kaUowYnFpOFlLTENFaXRBK1M2V09GRkVWSlRyZmxuVkphcXJnUmt3dUhhN08rWU1iL0d5RU9xZmxXSkNsWTVsMk0zcGVlRWp4ZTJzM3BsS1YxZk5VZTkyV0xpV0VhZVNWUDFmdmxCZndmSjJDWGRSZFVEaU9SQmdZakxxblkrS2orWnoxNjNIMnB3QjRhWHFEYjQ0MDhhRFpQUUhFbVJURGtPQ2hWYU84MzRTR0lLOW40YjJ5WGxySThjQzJoNERzMlcxNmdjbTdVZ1ZnZ1h3eW9Lb3M5S005TmFVTk1sOS8rcnduV2MydHhBQUJXTG1NVzE3MnlWOUdDUkFIaHNlUGY5NEtDVHpLaVhEOHliV2paYkpUSFkwcHR5a2E4bnp3VkdIMVp2SDlkbmg1S3I1WjlsYlFscnFncjNLNTlyMnl5YmVYcVdJd2RER3FoRlMydVJubDJoOHZlcnBJNC82bGhGQzF4c0lVamV6aGp4QjVPeGF2QXN4QStlUjNNM29WbHZqOVg3UStNc1dGbEhnWXo0Q3N2TXVvQitXSkxTK1ZGTmxpODVoQmVMSG8vN3pORmE0WFRSSUR5MTdUL3Rnc3lOL3hMQW5PSTI5NFJaYllJS1lkaWpQWmZXQlZPMGZqbnM2dlNxNjJzL1VtU2JXamdDbi9yMjk2NTFOaGd5YjNjZGNYMlpMZEJ5ZzVjeXNHRlNUY1QrT2s2dUtHUDA2ak92Y3pyaDdSMEhqMEFYUkVYTEM2NUQyQnNGeHpoc2U5N3hnOUdaQW1lTFV3ZDE4N1lWdWkvak5rNzQ5MDJkYnNaamFpMGdudnVISEltYThETDFiLzhBUGhLNXRSOW94WnliQkxuRjhWYUNSWVN6RVhtdkMrZExQSGppbCtJSVFFZnV5MjF4L1NnVHdQMFpleVRrOFZJc0tLc0lvRUlzWlhOYW9xTHBmNU1YUGxZL0YvUUlJQXNBeWJZZ0lHL0JuUWZnVlJDTG4zYkpJT0ptTUJ1S2wrWmxXeHB4U3l0ZWxnZUZ1bmtCZVY0QlAyblNMRWFhZ05OVG01Y1FsUEVxa092U2dnemtLa3k4Ui9YUHZYYkJTTXRzRDh4Z3hhckNFUkpEV3JDa0tQNmtCWmNsYUFxYlVpVFc5Y0hQK3RHN3dsY0xsbnBFQ1pIaUE5VXN1N3FwaUtnOURkdTV4QXVvTUtEci8rdmtDUllRRlg4ZDVHZ3RaaGlPQk0xQ0I1ZDlIUDFwbUNWMzJaOENUdzVHMGRveCtya29MYXNGRUw1Y2dGYURRUTZJSGhWVkkrdEQxaXFQNzZWaEJrRERTMWlTNHE1bWtmT2dheVRES1MzQWZ0RWUzeUExNFg1amtjL3ZuQjI2UGlOb2paN0k1N01jN1ZFWXFmazM2Uit0Y2lHSmhvWXNSTGdPWDNxM29Oc0F0eEtmOU1wY29LRjJNUTdYNzluWGxZaW5tdHFQbG5FYUdwY0VmOGVUM0NuRVJBSFAyZ0cwRUR6U3hhdFV4Z2piS08yM3FxMUkzc05HSmthQ1JTaitrWk5udk5JL2ZSWmJTNTZyK2xlemRWYkx0NGpTelZDTHh3cUVpV2dQUThtUS81RlF0cVlvcEhxQ1F1MXZIZmlySE84ampUSkUyd2RVdzRSRzQrSklKb3Q1MFhXeVlta1dtdTZiYkFQMlQ4ZGYwTVkzZW5hRkZNNldqQm9PMXgxUFRTN0pleDlMOW02Z09VNGZQbEJITDArSGdsTXdlc1VmY2FHK29VQzNXaldJcHZwMFR4amM1WFpQeDlwdkNsbENPTUtlcmJSZlA0U1JzMmhxNUpjSnJqd1pXcEZDUkQxRlowRWtjTmthQ2tlWXhza3NYajNBaDFWSjArWXF3cVNpTks3Z2p5NEF1NEUxUzNmTVZjMTRSRUVJcWpCaFdncnhSY0djYlpTdDlFSkxISXZ5enNQZlgxQ3RJZ2hTOUMxakQzT29vell3M2FxbHFOcU1RVHpjbDlTOUdwMWFYWnl4VFAyb3l1QklweDJRVXk2cDZWN0FpdU1qNzN5VVdEQVVDTWpvR1hSL3A4dzdOdWxiek9FY2J6eFJULzNSalZPeVJ0dG5zTXpCT3RTMzZqWU15aHZ0ajBqM0tmUlNJZFJ1b2c3RUhqUXlIY1o4YVRDRjZlUGlaL2JacEZ2R1dNS2hYcnhyU2JRcWhOcU10Q09lSVBsUFFHTCtidlNoQ1JiOTFvNE5pNWNTYVJ3WnI2bHVEQ3Bsdk54cmY4MHZpbzdMdjhCbnVBTDFwbWd0Q2FiWjlpVEpiVnFhMys3V3cyTUxweEZYaU9pYVl0NGUyb2NtemFaOVBoWUkwNUc3Ny85Mi9wZ0dKdHlJWUxKMFFYRzNRUWVPMkFWSkJmYUdjVnpoaU1mb2hsbEdrN0I3UUlidjVRYjczVnp2TUk1akZXdWRocUhzQU5ndGk1ajY5dTFZRXBKY0xIWUEvMmQ0OWZndmJuMHJMd0pPR3Uyb0FENktDb1hkYzVkTXRXMHE0TFhIMXA1dmVoNTc3OFE1b0hUeXQ2blFXSzcvLzdNMWJvdGlib3dNY0M4amJBMndDVHNPb2ovNGRXRTlVZmFFdUpGQzBYU0IvcXIvTlpYOVhNbXZWT1ZObG9qRkh5eEVnM0RwZ2N2bXJUSzl3UTM2UHpWWHljUnhEWkM3NFpFMFozVlVlOWMwRXYwTjdyclluTDlRZitzV09NMjZlRitYcUlzeVM3WVB5alFCYWhSTzVJOVg3UnRPRk9EUkt2Nis2TEVrMnAvODBodUp3d3BMZVNnMytoYVdBQzZOUlVPVnpnZEZTMUlIR0tyRGZiMVRHN2J2bitJdmcxNk9CUWhmL3NPZ2RqWnErb0Z1eWpBMGNUSW9LYmdOTjNGMVhmbk5VT2w5SFA2NmFya2ZSSjFTUEs0a0lBclIxbzY4UjczN0tSKzJmdEhEK1I3TnJzd3JOdW1XTEhYb2syTUZTc0pSVVFINEpGOU1GSkNwbWVnVEFCdElYVnZ1Q2JUakFYMGtmbVkyZ0JNSytRSHBLSnlKUkZRRlgwczNUTUdmUjRxUHhodmdleWd3ekNXSnZ5QktRbkdOa25CTmZGZWpMVWNEZ21hS0lyWUQzVU53ZjdBRm9qT3VOMlQwSDZJbFJuOElsR2dvOXgvamVQa0dML2FNU1Jub1B6eFJRd0lHQlMyMXlqWmNodmx2L0VCWkpLOVlaQk9vM3ZKQUVqZEllOEdEdWNkNEdvWmNMUDJZekQwS2ZHc0NFT1lYMVVkRzRSWW9pM0hQeXVYRGZHeC9YbG16YmJGWkNJL1lnWG5kcHZRaWZkdFYzbVgrYko5TTh6UEZyekRadTlrS3FXbXFFWDl3bSszRW9FeUpHT01oM2p6QjdTb0R2eW5oK0tvN0pDNnNycExubTJLR1FwdTZXeXFWbHI4MWlhNzN5QVBpL3dvMU9sNWNNYzlibFpxbGUxVHZKWkY1NWx3cnpRR2R5R2xLUS8vQ3pqeGF6RHNMNTBGejdWbnFwWGg0N3lmWEQ3VlBuc0FmSlVxbUhveFlQa29jOVliN1lLMmZBUmxxdG00aC9kdzVjUHVtVlF0ek9tL0p1YnJ4WE5tOGh6Q2x3U1ZVcHFYU3lhTEx0YlRBR0VhelVtVyt4cm9HVE9pWHczQkdtUDJRYlpmRXRHM0t0MzM5ajArTmtoTnFTNmJOdDFyOWhSelR0Mno3ZHF0VmdJZzNqUG8xUXZ3ZmN2MHVxQWFBUmFWOHBLRDlMUjVmUUh3cnhuUUR4cU5kWHk0Z2hSNmh2aURqTE1rZjdxeE9UU3RBaUl2QnJzUElqL0VEL0kzOW1lb05pN0phSjl1bmg2cVBCTk1hTFZvSUY4MlVFS1hmcUFPVlY2eGNXNWZPSG5zVDdXR1pkUmwyYS91M29OVGVjL0NtMUtaWXhOUkUxaXVKc3hnVk00NHNRNVRYbzBXbGJyU3dBNnpHcHE0d1dQZTMvTUJiL2JjaENweXh5Zm40V3JRMU53cHAybmVUNWFDamZVY0lyTVB2bTFOYjF2UXhrakluSnZua1A0amxoN21DMkFCUXpKS2hWVlRkOVFTeEtnUGpZS0VkbEZwaU9qWmk0S01aR0lhV1AvVmVJWlZvMDhKYnRwc0RBc1YydEdRRkxtdTN1SzZTeFlkZ0NnN2RPQjFjWUMxMi8zaERweUJBYVB4L2xsYlpsTytzYzV0bWIyNVZCZjFPdEtkdFd4QTJEY3hJdFJmYWdjb2JKVldWUnNXVW9xYXloTm5rd2RkNlFMUWFRTlNvUGVDNHViUUIzSGdWQ01yak95OUpmNExPOU9iTWQ5MUoySnRDRlFVV2MxMjdabndISTdNL3JBSCtFN2psRkduYm5CV0R0ZWZ3WjRLNzBGWjJ5S2ZheHErMTN0WDl3UFBZYldTNGQ0bVhzTjBTdm96OXhuR0V0aXhKdFJzbm1sTGNEQ1ZjY283bmpQQ1lWeGpYeW41eWhiV2ZSVHpxeG1VSUdzckVwYi9uZ2VqQ1RRRVEydm13ak1KejZSTDFMM3RCNURGcS8rTVJBQm9iWVREUExvZ1NMY1BZWUZGQ3lKcGxhbVRHS2h3djlsOTZISks2azVoTUJ4WHhWZlIyM1RBNlh1aUZlRm5tN01TZm93YTcvdzFlUVdiUjQrOFJpWEpTRTRBM2xtL294V0xsTFVieEVkYjUrZjhWdFFxczA2NmZmY1pXOWEwZ1JrT3hDSDgrTEhBN0FTZFBZQWFLM0h3ZUdKVXNzbDEvZSttSFF4Y09aTWNmNGlzSnNRbTRYQ3VKVU5DVmhaS3h1RUZiSVJMaXRGQWRETjkxSGJZRlZDRmE4K2p2OHd2VW1zMUlOazBPN1JIQlhSY1RlYm05VHJyQmU5VWhMaklVNHNBeTg0Z1M3NzVIWEQzYU5IV0RGcnhmdXFITlQrWmhibHpJdldNb3cvS0p2OFB6UEJubEJDbEprU2IwL1lrNWlPKzBzRzk5dm41bzBmT0tWZjVsTEZTbVhmQUhxRE1DUzcwWkkraEpkeFU0YldrZVFYYXp6a05DbElUSERHelk5Y1Q2c0lDazE1RW4xSkwvWXJaK3hibjBKOGhISHRybXlETTZvQnhpbmorSlBzU24zYkZDZlg0Uy9YT0V4K1VzbHlsZlk2UDJlS0ZCSFg5b2srOWRORjU0L05WSVBDNi94b0VSbzBRMGJZY3V4YXNBNEJsckpGWEFwa1hFVlZYVUphNzhMUVo4WE1xVnN4aFZjTHp2eFltZHViYTN3UFZNdDJtQ0x4UEswYWw0NE1hSGRoUERocGlYWUZSMmdJUkZ2V1AzSndpOUNySU5kclcwb3pXNEtwWGJxUzNIdGFkb2FKTVMyWWZ2enJRcHQyWUp5Qnl1cEkwSjR5ak1kYlFYaEdGZjVPOVhyYlQweEt3YkdIZ2paRXNCaWI3elV3a0ROdDRLVUI5a1J3UzFMMldQWmlDaHJrQkg1cHNKVE04akJNYnVBb2Vta3RpYXFpcDFHN09tcmE1NUlJMmZCdDBCQXpYZjVjZWFwSG8yY05OWUhBSXA4c3V5WmtHdC9kSEdlaXRneUtKeEdsNXpXQy9QSEJLcE9sWjRXNFcwSG1ydjhIVXZxRGp5YkJCTVdHeEV6K2tyVmJBSHRmVWcwMXhKL3pvN2RuQ3JNLzUwemsrNU5laVFzZzdIWFNab3JYTjA2ZS9WNGQzUFlJM01hQTErakkwSFBlRU1jYm44cVIra08vaHFLUFd3eHBITDhlY1ZNZ3BvZFBicmFadFU1YXNaN2NvRjVhL3V4VmR0NHNhRU90ank0UDBlcXdyMFZXZGIwYTBYQnR3ekZhYWZ6S0o2bGRYblZpTHc2c05Od1ZOTVBDSHh6UFRjQjhWRGIvc0dwSFlIN3grWVJ1cW5jVXNkWDNGdEF0UUlUSnpVY0dCMGw0Z2FZcTh5d2hiUnZ1T01xUDZyeDdtRUxUb3Jhb2tENW9Cenl0VzN0em13N0diS0t2d1J5NUc5ZXgwMFZmdndsWFFSUTgvUUhqWm84Q2RLYm56QUZ4MWdXVW5KRXZibUFpK3QyRXlkRnU4QnoxOEhRMVEydUJsQ3pMWW9kR0RTTS9TcWJJRTU5UGJhVXZsRDJFeFZDTEUzcnRxdWNieTJXcGwyZ3RRMVN2L1Z2RTY5U0o1cXliSk10NFVTN3BDbUxGaG1ycGlJRTIvSW80RE4zWkI5YWd4a3pwZkkvT1l1b1FnYlZtMXcxd21rcVpWMnpGdkgxWFZFMEsrSFkyZXZjaHllbjRTcnRRcGM1aGFaMGFxZ2tLaC9haHpCR25pVytOUWl2cVZhRWxEekNydzFSbXp1Uy9kbHY3MEE4SmxweEVlZHIyNDcyZExualZvL1Z0ckRuUENPakhUenRVZ1VsamExVE5zQm1qVG1HdzNPQTB6ZkwzbUsraVRCU0svUnZYMXcybmp5SnpIQUNmY29JL2VIbERHS3JtMGcxZm5uUGhETktkMFh6SmVWa2JocksvOEh3QytIN1Z6UUZScE94K1ZCSGdJeGtTMVdsdWpMaWpXVjRWclZpNG91M05BVVJUeG1mRlVDZ2k4YXBGSi9VYytLaXdhNXlqUTRMSEdRaFVZblVJQkwyREV2SGtMZXR1Tmt6ZlMrZFFlbmtiQk5CcWhwWExVdXZYMlBYcmpJRFRmZ3E5MEtBRHMvU2ZpdkZyWnFWU25ZdlZNNUhkZ3NZb3hTUFJIRkFpVXJmUHBKbjdHMUIyVGYzQ09RUEwyZ2dEWlRGdlZxK1RDcXNVVVA5MWpJZURNSTlQSEI5bWtoRitMdW0yMmdpRUFoU1ltQ29wR08remVhRDdjZnRrbXJzQnpDK0FQbjdFZWowcDYvM3V2N3dUUzdPR0V2L29jaWZtWVNZTVBQdHJrMnNqcTk0VktGWkc2SFAyZVlPdkdRUnVlRlZVV09VWkJHTFM5RDQ1R1N4V1hBQWJIRk80dlBoaTZuV2FYK1U0ZnhqYk8wTWVrcS9FbDJVWEpkS0psanc2bWV0R05FTHg1QWpxWmVrbFRoL2gyOWMxQUNmOVN1bDNMaG5RUzB3dmRTT1FpbHVEU3NaaVFieVQ4V0NTV1FDZ2FHbVViblFEODJJVkJNMzdtenltQ29SdWR1b2FzUkFOTW1pSEt2Q3RmRVBGMzJMVGptV2RyOHpyc28xOTZKS3ZFT0xPUFd1NVJmY0o3OWVIUVN4d2x5OVdML1NlQ015bVZGeGlWM0VRcmZlOURWRmVrUDVoUnVYa3VaZDdHY0dicjJ0dm02OFBSODZIQ0NsdVUvNFhrR1ptbks3RUpHZ09pOGlkQWVIR1BsS3RHN21pSW9wQTNwczZxcS9nMFVLSzhLdCtVKzR5YkJRdGxhaHJnOUQxRVpBZlIxZ3pFREJZRjhNM0NVODFsc0JtemVlYkNJN0hkb3labVBVUnpVMXhmcHZuaVZ5VWowZ2lzMll6WDZ5b1g2c2dCUGY5VDhORjBuU1N5TGVWUVlnbENpSzNaL04zOGhFV0VIWWgyQ3RTaURoUE1TUDlXNXdSVG1tallBUWhVLy9uanAvcUtjbGdVNlhZV0hlcnlwMnI1cklvMmxJejhFM0V2MVgyNVR0eWQ4UlVsM09LQ1VJeG5kdkNyL3laYmZTVDVpNUVkVUlJMnlkOUQwNE8yS2tiM2tLcVMySXFsdW1RUmZvaWFPczlHSmRzTW94WlhIcXp1N2Y1VTJhRnhhdHR3VkRQTVJVSERHR1JxOVZNaU10Rzl1T1dTUENmc2E5RldnSzhoWWRuUW9RODNpQnBOc3RuVlVPZVVwRHlhWk41MnJTMjJiejI5TU1IMUl4c0Z1N1ZpMFQvNitkeGtJblcyMjAxMGlxNHd6L3lpYWJKL3RnWHBnOFAyaWUrZWFKRy95SGdtaDhsTkkvOERpTmtkUWNqbjkxNW1ZL1I2bktXOFVMakVERzJROUVWbnZFVEZKVXVqb1ZMUlhXU2FPUjBtMjZoY0V3THF2dUlocHJTMnB1d3gwUURaVU03SkI0cCs3N1FnSnN2N1NTQmdPaTVFRnFKSWlIMkVWTEtqYVFTS0F2SzByWDJocVlnYllRV25sMjk1R3lTVmlxK1BtTHVYRzBCSWxDSC8yTFY3a0JDVzFTQjc1OHphSG1TNklJeVZYVTV5NGN6ZCt6b1A1MmczWHRsMTMvQS9TdFU3OTBVeWU4MW95WmZmRk80YVNETDZnWTNNQm1yMlJnMUlHUUpxdXpqR2NwOHM0cTFDb0laRlJRciswSnk0S3FRdUJtK1hWcElzL1N1M25WcnRxNlVOSmFoYitmeTVlWlowaE56SElXcUlDenR4UEF0ek1FSUhNaDV0UWVGNzVhWnlEbGJMUDk5T2thTFdSS04vVjI5bytIUHZBLzk0anh2TEpBa2R1TDBSOVY5di9SVHc4NkxYL3VnQ1ppSjUyQXlvdkhxNngzRUpGZXhNaDdPN0FhZlA5SGFMWXNTYTBiaHBnVkhBVklRL0o3UmNiV2dqeW1EeHZKY2tubWQ5Y2wwZzhaWncyTGlrNmxQNmlvYXF1WGlIQ1hRTFBCbFNweTZib1BpaFNFV3pzVTZlMjdlYzJWa2tNcnlOUGhlU0VZMVhpRUpJdDlOZWo1SE9IRkZiRFRTaE1OMzRFRWs4b2VBczdoenRXY2dLZWRQQlh6SStwWjZGQ0JlUk1GNTRFaG1MV1E4MVB1MlE3SjJubmtHSE5HOFlDaHlUU0FtTTVqZy9ocnRwQnlxZkx1NDlEd3VmMXMxVE5WUWlNVXZKMVhMcC9sVEhmc1JncE1KSHVLQjFFS05rQk9JSHU2R0xXYytJS0FqWTBlRzVkd3k2Z2lydCtqMlhnZ2o3SjBiUzk2QmwvSFlpMSt2WkQ1d1g5cy9QeFJoZGYyeVNuMXhLYmd0d0JTekorUmJ1TXk1QkRuc2cyMzA4eFNQSzh2bnFYWFIreGMvT1JXUU5FOGNTVEFyeXZhVjRydDcwT3hveDlldXVoTmhHRGVKSE95czUrN2hjMEcwOWVKWWhvVm8rRWk5dDlaWTRoMEVLRHZSNnZFK1VxS1lTQWw3TDkvbm9HNTFTQTUxRlhLeWtjNnhtZFMydm1oMVpXUTdZL1drY0NKSERMM3VwU3Nid2RMOVIvRVM1elV2ckhjWjcyYktSODFsbEluREViK2RlSXd2MERYc1FOZUNUbDVqaFRHcHJ0V1dYWGZKa2I2RmJvejB1QlpySlQyUlZoQzR1U2U5WmtJeW4xZTQ0NDVpa2s5c3ZicUdERFJsajlvMldrUDRnZm9pMmtWWUMybGIrYUJYUDBJam9oOFRpbnh3TXEzOFk2alF4Sk0reG01RlVOOXVCeC84Zkp1cHQ4MWxKeW1pM3R5SDlscTRQQ0pSaDBaSGZrS20yNm5MYzJIUktaeUNBRjNvT01EOWFRYjZ4cFNNOEc2SFlDTDV6Z3FhdEtzcUFDckdEYjVpZGpHMGg4eW42eEFOVTI2eFlNcFRHczJDV0pLSCtZZlRqTnZxaUZ6S0oyTWttMUVJaFZDUlRvbkpwYWZPVit6alV1RThXeklSQ2czS3BvdjBjbkhrTDRiOStGRXAwSkR6V0tHV0xjQ1JWQXV4eHZJUnprUDRnRllGRkpDaUhMRmlydi92OWZIRnY4bXVZSityWU1XNTJPTDRIQUNZVkx2WkhBVklMVHpZdGIrRTNlT2N6dHNKeExrd1RTaDNFYmxMVEIvdVNZVGk4L01OZmRySVRud3gyZ3YzUjNmbFo5ODBrOEI4dCtzcGFOMzFmNEZOV0plSHY4YnBkYmE4YWxwcjVQSzBFVnBCa2lNdzdKUGFsSWdySFlnMGZ1QUdMcS9WL2NsVkJHa2V0dVF0VUJTVCt2ZmdaaFZ5MUZicGo1US9ZQ3NJcjJtQUR4ZHZOMTd4YlExaXZxRHRrQml1ekt6UHhMckk3Rm9OOXBNQjRPRDF5M3JUV0hoK0gxYk42VDNqRVpXVlJUVXlYV0NSL3ByaWJDK1ZRcW1QUFJNWUtkZVNOTk1UbGR3NkpTb2l4T2cwSVRDYUx3elhlZU1iYTNKYUY3dllFc212YnFySmxRRzEzTHdhTnB1MXMvRWxzUERwVDk3WjJLNWRLNXk2OU9NRTI3VlU4dXlnTVk1eXV5NktpVmR1L0Z4RVdBMC9ndFR3WFdZbXBPUlBPUkxpL1E4UnJ1UDI3RUJobmtpamZuUUx6MUZ3WTFoMjJqT3lUa2NVVVBMZDJvRjIzb1piU1RPWE9OaHlQYk5uMXM4Y21UbWNjaldsRm42eTJyZHJNbVB3cW83OWxUM2V1cDFGZ2FqYUxHZzVIOW94T293anl6UXU0L2s3NmR2TmhNYzZwRWp5SDNVSmMxQ2pZZmNxWlI0WUp1WDBXeXV6Z0RtQUs3Z3UrR0NRdVFBdkp6V2FFR1k0bDZzRUxUWUNRL3VIcmovdjIrTWFWbDdyQ25PeXUzc0NCU2RieTJHckRsMWRPT0FETXc2QXFYUE5WaXJYY285KzF6OHQ4SzZvYldmRUw3QlhIemg4dFpzZjZnRm96QTlOYk9mUWI1Y2xuRHU0VHhMa0pFUmtWSW1xVE0yd2pJQkNTaEp6cHdkaVdoUUl6VXMxRFc5NE9RazgwcExwN0JwcFBlTDZEN2c2T24zbU1WeDBsM2tLbi9ZZFJ0N1R1eXRkRFBsY2ZDWDlnWVdISTZOUzFhZ1JDZG84bU91b3hFa3JrVi9Rc1RHdmdRS1pTcDdGVEx2cEcyQU9oS0JnblZQZFY1T2pVME1JcWNuOGY3aDJMWkUwV2hqWGEvbU41Wmo2MG9yZkhEWXdhWUFGZ3JReUkreE9SOWR4S21VKzJWb0tJR3RHRStPbGlnMmM0L0Q4YW1iMzNaS3JiVzV0SFpYSVBwOUJuV1RVOS9qVHFSMTB4STJBQ2U3cDU2c2gwdU52UnRqbnZHbkl0dnM3VXRYbUgyT0w1NGNJdkVJZ0xhanlrdm9tZ3IvNnIzZW1OblpxTmpiQWFOSlNZOU95QTVkN3R0bWJ6cVQzb01sU3RHdThIaDM3ejlmTkZMYWJ0K0FvYTN3SzRJck0rQjhnbGxTRk1nQ1lxSjc5cjhYYmZGcnd5ajJqaFo4L0RNNTlpdTNoMHQwcGJhQXgwRE00dFV5di90YnFYaEYrbS84WmFPVG1vNkRBbi80TC91Y1VYQzNJWmhQNEFyS2hxQmtxbmd3VXhzSzVHdVg4dmMrS2lNNmJWZUVLRVFhcGZtMkhkTmVvK3pWUUdKOWNmQjlNL3NlNnd3UCsvQ2F6M01qRGppMHdCd0IvNnZxcTU1ams2eExYYjVaeS9XdHVsZFFqQnZ4cm83Z0o3dDZBM1ZHYkhoRXZKS2xDRHk4V1BUMUhib3FUc2xadU5sRGZZYi94d3hCd25aYmg4YUZCZEZwS3dMZDU1MUJCaVJGUWMyN3VYK205NTQra3pTNk9rWi96VmZ6RU1aWXFuOVdCNGR1ZTdNRTNwMzdROVAvcnlXeTY5bTJ0eWF4eHVqa0lxWGRGaWdycGVXUjhrRTl4MDFtRDdpcnJRK29nYk9tS2tQQWMvemlBSEJKdmZ0RFh5RmhwRGIrRnA5TUhNUUhpZHRPMEpvb25qTE9meXpLbUtWVER4ZzhUSzMvdUs1YjNhUWRGd2FCTXpQVzJJb1pONFpXaTY5cTBCU0FCREhZK0U1K1BOYUxpTThkR1VkWjBLaXMwRitDQnE1Wm01Sy9XeEVNdm0xTTNEenRwV0hheHpiOUxRTmZvUUpRTitQQW9nQklWcmpiWG5wV0ZQZFl1bURqblRFaWIrWnpHcTQ5bWRHQTdhbk1OMEJHaXl0czlaNld1WXJhZmFNS0p2RExkb0dQeHZMZ012SGVrUmRWZmpCd2pBNFlGa1ZoNk54WjdXMFFiN2EwdUdBWUc0YktscEFIZ05URE10bHc3dlE5SGpiSWdTNzdyOEFzVzc1enlxeHFoMjg2d0YyTGRxQ01sWjRGS3pmN0gxcGFuYjI5MGFSa3lvVHpJSi9WdDRWVHlXWnF3TEFzUlNwa2lsK0Rwc08zRXdQZHpXaGtjVHRHWDV1aEpXcTRHNktkeVZQd3ovcEtEcXBYOVFWeWp5clFQOVJCejI3aFE2N0dTc2hZd0lDcDVoNVV2QWYyUzhkbEQzN1ViUGpDVnRYWVpwVUtoYnJlNmZycGRrREpQYVM1RzVwYkVBYWNESEVUY3J1ZkVTWnlTWlRKU2lFa0JRRktvb3hhNWpCVGFMdFI4RWJ6V2xleUp5dXZ5L0tnV2YzSEx4SXQrZVlPYmNXc3ZybGwwUWVUOUVDakNCclZCcmN4d2h1V2ROZ2FpNjdjUExlcUxnN0FYQmN2M1BNTDNtMkZaUUc5RXhpa3JTaWVjUVkrNzRMZWVIOW1XYjhPckVhQWJ2bnFHNzBjRmxUZ3pLWmtBaDkxdUdDM0psY3ZZb1RmMFUwS3BueWV0NmtNbXBpVkhLbzFLMEo5bjExTVlvVEZrN0JkOG04SXpUZ2hMaHo1S0ovRHpIUGQwdzBFTjBDV1BUaUNub0pwbUgvV21IcjJWZ0FITVZ6VmxBbHgydG9vSDROVW5mUzk1bnJYZWxib3dFMUZOdkVLcUxQbXZNT3NjVG91UnJxUnEzaE1tc2pueDFsK1hzempzOHo2Z0YraGlySm4rMnFsTFdoYk1ObGIrdkFkc09QY1JkaC9CYWQxTmdIVXlWM2tVcS9LREtnUkRIRlVIRE93TWdJMGMrTStTRDE3SEhiRFU1OU5JOGpzQjdnUXNQSG1scm1CZitwekQvT0pmb21ZdTlaeUZ4NUZYT3NpRHRCVDRaaUJMazlwTHY2czNwYktkVGhzNk9jOG1rMDU4VjN5VkVvZ2M5bFBnTE1JR3ZpTkhYK01LTXZWMmpncWNIQXZwUm15eUt6YlkrKzluNlZ6eis0Unh5b0QzcTlxUXN1ckdrdFRUcVV1UkRJZFRnaUlIVkRuN0lDWFFBY0V2QmRMSHNqODJrTWxRYzdHV2RVOUFnTXhsbXFDdEZxbUUzdDF5cVltblJoVWtSOFZ6UnZCY1RBdjdBakJmZWo5cDlMQVFET3pHbjJSeXg2bUphaFMwZk9jVGdJaVQxQzNDUEtpOFFRZmVNV3BZak1pRTRPYklsamw0SzBWUEFXTGluQ2dvbXI5TERNNGFFdThTMFBlemt3WkVLeGdXR3VYUklVcDBraHltVHg5VlhiSWV4TXdOaklWbXg0K2ExbWpTeU83VHowT3VJakM2cTc4RGFseTRJRXc0MFhiOFEwbFdRdTg3R3NlQ1BPWkMvNGFyVzFSM1M2eGF1OUtTVkF1d3YvbzhTL29mK3ZENVpkV0FFR2wrWnZVZnlDc3ZBaVM4NlBiUWVVdWZQMUJZbWIyRXlRYnVUVWQ5OGVMOExsaysyNTJPYWZGZUZPWldKb3BLV21XekxvZlJzcHc3dHlzZ3lIS1gzdWJGcFJTemJZaWEyT1QzYWpzRnVTOWxJdEUvWWJXSWVDTXJrQU5wbzhnaUptY1pIdHF2d2E2MnlIS1duendzZ2hZeVhIVUlaZGJLM2NrNytmdHJSZmNlaHY3R2IxZithalBhRjNJUDUvTE5lbGEvYmFUZVl0cG5LQWlpNHJEeVA2RzhRMDhVaEV6UlNxemRNYW1GTnhRdHRzMGtOUU5ueVlHVmNKK0RTbnphSzRNcFhoc1ZibTZvVlMyS240am96OHN4SndxdVo0aHlPbkRpSmkxVEpHd3BJSEp2YzhtTm9EWExQWG1Xb3pqL2orRVVLVjlsVm9MTzZrQ25EdnZGQytQOFQ4ODYveVJQcndNajExdi9mUUM4MjFpY1pIQU9LRVljL2poSFAvenYyeVFSbDJWQ2ZjQmxHcW9Jc2wyNkxYbitiOThURXVwVE9ML1kvbUFWeXhDSUs0enArWTFPbzBydzBkK3Z2OWg2dVRzSFFadWFGYTFtVDY1U3FNdUN6S0pOUTNCU1QzcXZVYlY1OUR0aS83TzRCRStxU0FVa1Y1YkkxVjNpYnlPUmIwRTdkaG9SL1RsRWxaVnd0QUdPZnpmYnJ2MGMzelVhMFVWZEp2YXVwdXFCaURZaldscGlJMUllM2YzRU5VbW90dUljeVkyMmNKNGJvREdOaktubjdzUVNLZlI1SlFqdDJCelRtZXRwNzV5OWpOVXpyUzlxZERPRFRVYThZOGtUWTJtWGlMZktzUmo3V1Nwd0JPdUNpNWFPQlNKNnhJcHVKd1pHbG5WU1REZGlPQnN1RTYvMkFRVmoya0RrZ1pwY0dGQnpSUkkyYWpZUzlZdVlnbzYxc2F6SWZOcnNqOE56cEI2SFhQUFBSM3RadTVyZFovRmJBaXJHMkRBY3AxNkR4TktLZk5EU3FFbVVza1FDYVJsSGNoZjJRb1JUdytHenpHV2N0QiswVkFuSWVKMnVhVGhWUDE2TWY5N2FqVHpLajJoQmRYc3RRaWc5MjE1U2d2YnA1WExwQUtoMjJZVEpLODl3aTZxTS9nVVpWRGJiQlRRQmY4dTNYZDRNNENuTVN0eHFFZzE1QjdIb2ZmNVRlc2ZhUlhkZ1NOVnNMaUhCOWxTV0xXYmVSYk5VaXgzS003b2VCK1IycTExY2tXWVc3Z2pxVVFnOFVLWlBGdDZpeHNCVWRGMUl5UkhZSG9pMWhOU3c3Smtyd3FhZW1DVnViM0szVTF1Q1pMNjR2dlRjZFZhcU52Njg0TktkNHhDOU9ZNVZaSC9DaHl4MDdEdzFIMTA5a0FhVGxHUDJCTkxOZ3p0QzJJdjY0L1hqUHlZTWpRblBSa2dVbEdpaUNBNU1IY1M2RStyaW9ta2dRekN2dmxndTlpYWhkcjNWOWl5cTVhL2RzRmRFV3lnTlI0ZytMMXh1Z2VzMjIwbGVwVWRocmdTR01kR2daRlZLN0NuTkRNZUt4TnpYTkFvZVM0aFY0YWNwUDAvMXVCUkR0TnYwZ2Z2ZEZaT2JRL015OCtnQldnWkdyMy93aGpTUjNSamVLM2RVajVWMURkdUNpOGNuenY3d2tSYjBtWnRUVnI5UjhZWjdNRGlXOWZNRFZaWXlOYXdkL3BObG5TVTBsdSs1YkdxbTNaNTFwTmFtaFE1VEhwai9rVWlGbTQxTkhRK2dxakFORnN2UmR1YTI4RVdDbEZpdHpUTDhVb2xRN0Q2VlRRQWZjaVpkd1E1T2ZNSjg1aklBck84dFRTM2VkSEYwUjNlK0M4Y2VZNlJVc2VTbUp3Wm1KOFFhaFFkU2I5RDAza1V4NmVObU1reTZYT09ya201WjA5QWZMZFA4QnZMKzlhVTdRamlneUVsakU1YUQ3UlJJbXBsbVYrL1llcjJza0twUnpBNlpLV0EzNHU1RTdlSHEveDNqOVBsczBoWm1jR3kzRHpoV2cweFVOVGIwK09HRjVDTEpJaWdzUmlEQ0dLUXNUSTlPdklaSzljSElCdE12STNxckp1S0ZjTU9saDVVTnBVQUUycm1aREltaHRXdUtkMWhPalA1QUd5UVJSUk5weTAxTXo2S01NNm03bGMzQm9MUzh3eWR3L2Z4VTBmaUtpM0FsM2JKK2xOWmJ5ZVFpSnY2eDBMcDUzQk1QdmMwZm5KZlhDQnVQcjVFOFJVcXJhbFVTdnRkSG1PS2xGZlVPVktqZG94TmRKWnF1LzloVU1wVDVYQjNqbnNMQVpFbS9ETGNKZ09Qd3lMUDE0R3NnZEw3WHlZUVFVWU5wUXNOazIwK21DUkRIZnZod2JreTBIak1xK0NoS2I4TTJiYzBLSEx4UGR1TFlJWGpTTy9YeWxmMkl2ZElkTFQrcG4rK2lQaFUzS092RHg4Q21sdTNKZG5oNzdZTFNqV3gySFFudVQ0cVducFNNUDZLVjhWdHUxMUxibkZYZGRDY0p0L1pYcnI2SkZINlZVTU42UklRdlVCdlJJZUdTRzV6emNzY09oWnd1S245ZGFiMnZIWk9xVC9ZN0ExeDhDRGxYdWg4MUZLSnlIYmlyaHUza2gyVWdpeW8wS0NMbmZJb3Z4cXZhbjVhd0kyVDJsQVRyWm9qYXc5S09LRkNZZ0c3L29FMHAwbDltRjMwMFRiUy9MQkdmWGhNZjdZNTRVZlFYaWhxcUtHSnRJZU5kc0ErQ0w1V29ZSkFBSDZGejd1WXlDZllFdFloSDNNVGYvNks5QUE3am8xMkJBVnBPM1pHN0t6SHdrZWtyUnJZY2hRYm5tcld5aHkwb1h2b3B6NVFzQWl5d1lBM2NBbWh0b2N0eThXdElRVVg4bWpZait2UjJTcmVlYzRHZDU4eE16R3U5NzdQeCtoMEY3Q3BQZS9TS3Y5SGwwRUNtYVJrTGZ4azZOYitmWjVLZ1lkTUQyRngvN1haNlRJN05OUzJIYi91K2hHUVpicGZ2clJiTWxxS21aWVhacmVPRVM4bC85S2RGc3VSSndlYXVqYUhwTWVOa3JLYkQwS2dHQkt2Z3NvV25oTXovaUw3TXYzRTJGcVltMHNobXI3RVFveWt1cjRZSUw2VlM2RHR2bnJVVVR4ZFlUMkMrT2t1S0FWRDdJckhBVnlENTd2Q3drWk1HM1pUQXFwWHlweHFhL2d2c0hEUW9rd2VLVkZ6U0pNWndVU01Odytua2dZQVdqckg0Tk5FR0FucGkyUWJ0dGdGUzkrb2o3bGl1TDdCN0tUS0M5YjBwSFM2c3lLR0FQY0d0bjFXMUNYYUxlb0ZkcnJFVk00dk9LQ2svekpkSXA0V1lxRGVMUVNNSllFOS9JVFlpN1hYUitoVWxId0UxS1F6YzhvNHF4dURlWTVINTBlenBSdGt2ZVRJUjZjayt0VmxWTyt0WGlURGVvOHhFMGRtemZ6UURkeDNTKzZ3blBYTmRwMzJMM3ZsRUcyOG5LRE55azgvUDcxam9ORW1vMWplUWd3UVFjYzQyeDJkOEx5UjJOL0FCOVkxVGVJTUI5WWZ3NnhTZHZNYkJGMmlrTjFhSVg1cDZJZ3ppeVExZHMxNWhJRDNQd0tKbWlyZU1PTDlJY1lTT1lDR0pJWWE0R3dPVEFMejN5Wlk3WW9IcUFFVGZLd0FsV3gvQnNRQWFoYU1iL1R5bU9sZGoxNnl6UFhWUldaeEVBY28wb1l1bXBPTGdTeVZDNmZsQS9zNU1yeXMvdUtuTzYyVXk4S0ZmdkRHeGlxSlY2Z0dKQ0xOdmVmRjJXTlNkdVIrR28yaW45VmpCNUZwODFxY0JOTUhrRVJYbEtMTTRGc3dtMDBPMmpwTVl5TDEzNHNDVS9lSnNXcG9HaGIwM3dWTGNLLzNBSFRJWktkZEY4ZlVmSkp3U0F2cW51M050dW8zR0pyZE5mZ1BRb05TMXZMQWE5RGxVZS9lV1BRME1jMFE5MERzMEhMYTIvMXg5ZEhFUlZKSXdxaXgxV2JmeDYyd3RPSWphZDgxQ3JzZjhzM1JHaHBEanNQOFhiUkIweGxrOWJvTmRjUVZUVW5WdVBuQzN2aGdScjBXeWdtVVFKdFJyTVI4TExMSlRody9QZkJqem5lOTRxUWxod2hRTXhhLzFiZWxQL014UUJLd1hEWHZvYjdiZjduVlJienBSZVpCODJ6NFBha2xCOUR0dGhObGk4OERHeWV1N0ViK2pQSFpCcFBDUG5uS0taak1JUkxwcDQrWEVkZHNxL1ZEZ29ER29aNy84ZGxvSzA0QVhwRk56aEVFQm5LQ0YzUjU0ZXl5Sy8rUTM0QXpwcnpYVWxucENhUXZSVllET3RGak9LYjFENEozQ2FrYUw4YU1zTzVuNmhKKy9wdDRVLzdsMUVEbWJoN0tRczhZQWpIL3lTa1YrbGlVT2ZtR3dReXdzNk1BdVhpWG1IRmhUT2ozL1ZnWWVVeVZvVnBVMUpzdFIxOGtLbVZrdjZhOVZHdFBZZ21zMXFUVmpIanl1YVd4STIyYjFjbTd1ZTFFamsybFVsek5RWEJQTVAvVm9IaVpBeGE4M1NtMFB5NW41Q0c4V25uME9ndHZ0NGFURDBoWSs2MVBsbVdoNUpZaXZZWDMrNkxiU3k3OWNQMlJndmFtbm5iVDQ2aEMzU1djV25VcDNJcCtxOGRFSVpVZTVYNG9naS95VFlNdFBSWXZlODNiTUEzdk4waWNtdGlxWFJVY2JzZWlQQVM4Ty9IS1JPS2haTHgvemY0aDh2T21nS3ljdDJiY3lldktUcTIyUXZIMGRaTlhBZmxHWWJrVmxtT3ZHVmJhQmJhb1RWOEl3Vk92L2pyRkRKekIyNWlTMEpKZThZOHRJWHk2VEhzcnZFdCtJZWozM2Z1UHdnTkk2Y3REUG5MaGZrV09lMGtXdGd6bnBMM2Y0S0ZXYnA5Sm1qWWEwSGYxZXNUMmhXTW82Q0w2U1NhalpUNmh0SC9SbiszN1VTV0ZFSXJucHBqZGQ5K0NUR1FlSHBWeVFVR1MzZENXNHo3end1VTRtOWppM2IwS3RrN3lJNkV6T0VCM2lwTVowVlJCWEVCZFlRTFdRYXBOeHdKNThvbkxQcnJKM0JobHg2NnJlbVhsOU5NV1h6c2pUMXlPb2NIR3U0VmVFNk8yeVovNXRLUXJCRnR2ZER1NENTUFRxNjdxU2NrWTZZWkxLSTg5VkY1OW9HZFdhVXRCUT0YAQ==" } } diff --git a/internal/testrunner/fixtures.go b/internal/testrunner/fixtures.go index 7fac47b..64c1e05 100644 --- a/internal/testrunner/fixtures.go +++ b/internal/testrunner/fixtures.go @@ -12,10 +12,11 @@ var fixtureJSON []byte type FixtureData struct { Vault struct { - PublicKey string `json:"public_key"` - Name string `json:"name"` - CreatedAt string `json:"created_at"` - VaultB64 string `json:"vault_b64"` + PublicKey string `json:"public_key"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + VaultB64 string `json:"vault_b64"` + ServerVaultB64 string `json:"server_vault_b64"` } `json:"vault"` Reshare struct { SessionID string `json:"session_id"` @@ -35,6 +36,7 @@ type PluginConfig struct { ServerEndpoint string Category string Audited bool + APIKey string } func LoadFixture() (*FixtureData, error) { @@ -43,6 +45,17 @@ func LoadFixture() (*FixtureData, error) { if err != nil { return nil, fmt.Errorf("failed to parse embedded fixture JSON: %w", err) } + + vaultB64 := os.Getenv("VAULT_B64") + if vaultB64 != "" { + fixture.Vault.VaultB64 = vaultB64 + } + + serverVaultB64 := os.Getenv("SERVER_VAULT_B64") + if serverVaultB64 != "" { + fixture.Vault.ServerVaultB64 = serverVaultB64 + } + return &fixture, nil } @@ -52,13 +65,21 @@ func GetTestPlugins() []PluginConfig { pluginEndpoint = "http://localhost:8082" } + id := "vultisig-dca-0000" + + apiKey := os.Getenv("PLUGIN_API_KEY") + if apiKey == "" { + apiKey = fmt.Sprintf("integration-test-apikey-%s", id) + } + return []PluginConfig{ { - ID: "vultisig-dca-0000", + ID: id, Title: "DCA (Dollar Cost Averaging)", Description: "Automated recurring swaps and transfers", ServerEndpoint: pluginEndpoint, Category: "app", + APIKey: apiKey, }, } } diff --git a/internal/testrunner/jwt.go b/internal/testrunner/jwt.go index cbbb508..68ae988 100644 --- a/internal/testrunner/jwt.go +++ b/internal/testrunner/jwt.go @@ -10,6 +10,7 @@ import ( type Claims struct { PublicKey string `json:"public_key"` TokenID string `json:"token_id"` + TokenType string `json:"token_type"` jwt.RegisteredClaims } @@ -22,6 +23,7 @@ func GenerateJWT(secret, pubkey, tokenID string, expireHours int) (string, error claims := &Claims{ PublicKey: pubkey, TokenID: tokenID, + TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), diff --git a/internal/testrunner/mpc/wrappers.go b/internal/testrunner/mpc/wrappers.go index 66e4b2f..c4c21e2 100644 --- a/internal/testrunner/mpc/wrappers.go +++ b/internal/testrunner/mpc/wrappers.go @@ -112,6 +112,73 @@ func (w *Wrapper) KeyshareFree(share Handle) error { return session.DklsKeyshareFree(session.Handle(share)) } +func (w *Wrapper) SignSetupMsgNew(keyID, chainPath, messageHash, ids []byte) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrSignSetupMsgNew(keyID, chainPath, messageHash, ids) + } + return session.DklsSignSetupMsgNew(keyID, chainPath, messageHash, ids) +} + +func (w *Wrapper) SignSessionFromSetup(setup, id []byte, keyshare Handle) (Handle, error) { + if w.isEdDSA { + h, err := eddsaSession.SchnorrSignSessionFromSetup(setup, id, eddsaSession.Handle(keyshare)) + return Handle(h), err + } + h, err := session.DklsSignSessionFromSetup(setup, id, session.Handle(keyshare)) + return Handle(h), err +} + +func (w *Wrapper) SignSessionOutputMessage(h Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrSignSessionOutputMessage(eddsaSession.Handle(h)) + } + return session.DklsSignSessionOutputMessage(session.Handle(h)) +} + +func (w *Wrapper) SignSessionMessageReceiver(h Handle, message []byte, index int) (string, error) { + if w.isEdDSA { + b, err := eddsaSession.SchnorrSignSessionMessageReceiver(eddsaSession.Handle(h), message, uint32(index)) + return string(b), err + } + b, err := session.DklsSignSessionMessageReceiver(session.Handle(h), message, index) + return string(b), err +} + +func (w *Wrapper) SignSessionInputMessage(h Handle, message []byte) (bool, error) { + if w.isEdDSA { + return eddsaSession.SchnorrSignSessionInputMessage(eddsaSession.Handle(h), message) + } + return session.DklsSignSessionInputMessage(session.Handle(h), message) +} + +func (w *Wrapper) SignSessionFinish(h Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrSignSessionFinish(eddsaSession.Handle(h)) + } + return session.DklsSignSessionFinish(session.Handle(h)) +} + +func (w *Wrapper) SignSessionFree(h Handle) error { + if w.isEdDSA { + return eddsaSession.SchnorrSignSessionFree(eddsaSession.Handle(h)) + } + return session.DklsSignSessionFree(session.Handle(h)) +} + +func (w *Wrapper) KeyshareDeriveChildPublicKey(share Handle, derivePath []byte) ([]byte, error) { + if w.isEdDSA { + return nil, fmt.Errorf("derive child public key not supported for EdDSA") + } + return session.DklsKeyshareDeriveChildPublicKey(session.Handle(share), derivePath) +} + +func (w *Wrapper) KeyshareKeyID(share Handle) ([]byte, error) { + if w.isEdDSA { + return eddsaSession.SchnorrKeyshareKeyID(eddsaSession.Handle(share)) + } + return session.DklsKeyshareKeyID(session.Handle(share)) +} + // DecodeDecryptMessage processes an inbound relay message. // Wire format: base64( gcm_encrypt( base64(raw_payload) ) ) // Steps: base64 decode → AES-GCM decrypt → base64 decode → raw bytes diff --git a/internal/testrunner/participant.go b/internal/testrunner/participant.go index 9f52f7c..e50b0bf 100644 --- a/internal/testrunner/participant.go +++ b/internal/testrunner/participant.go @@ -12,12 +12,15 @@ import ( "math" "net/http" "slices" + "sync" + "sync/atomic" "time" "github.com/google/uuid" "github.com/sirupsen/logrus" + keygenType "github.com/vultisig/commondata/go/vultisig/keygen/v1" vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" - "github.com/vultisig/vultiserver/relay" + vsrelay "github.com/vultisig/vultiserver/relay" vgcommon "github.com/vultisig/vultisig-go/common" vgrelay "github.com/vultisig/vultisig-go/relay" "google.golang.org/protobuf/proto" @@ -25,377 +28,416 @@ import ( "github.com/vultisig/plugin-tests/internal/testrunner/mpc" ) +const maxErrorResponseBytes = 4096 + type InstallConfig struct { - VerifierURL string - RelayURL string - JWTToken string - PluginID string - Fixture *FixtureData - EncryptionSecret string + VerifierURL string + RelayURL string + JWTToken string + PluginID string + PluginAPIKey string + TestTargetAddress string + Fixture *FixtureData + EncryptionSecret string +} + +type ReshareResult struct { + ECDSAKeyshares map[string][]byte + EdDSAKeyshares map[string][]byte + ECDSAPubKey string + EdDSAPubKey string + HexChainCode string } -func RunInstall(cfg InstallConfig, logger *logrus.Logger) error { - vault, err := parseVaultFromFixture(cfg.Fixture, cfg.EncryptionSecret) +func RunInstall(cfg InstallConfig, logger *logrus.Logger) (*ReshareResult, error) { + vault, err := parseVaultFromB64(cfg.Fixture.Vault.VaultB64, cfg.EncryptionSecret) if err != nil { - return fmt.Errorf("failed to parse vault from fixture: %w", err) + return nil, fmt.Errorf("failed to parse user vault: %w", err) } - logger.WithFields(logrus.Fields{ "local_party_id": vault.LocalPartyId, "public_key_ecdsa": vault.PublicKeyEcdsa, - "public_key_eddsa": vault.PublicKeyEddsa, "signers": vault.Signers, - }).Info("parsed vault from fixture") + "lib_type": vault.LibType.String(), + }).Info("parsed user vault") + + serverVault, err := parseVaultFromB64(cfg.Fixture.Vault.ServerVaultB64, cfg.EncryptionSecret) + if err != nil { + return nil, fmt.Errorf("failed to parse server vault: %w", err) + } + logger.WithField("local_party_id", serverVault.LocalPartyId).Info("parsed server vault") + + if vault.LibType != keygenType.LibType_LIB_TYPE_DKLS { + return nil, fmt.Errorf("vault lib_type is %s, expected DKLS", vault.LibType.String()) + } + + err = deleteExistingPlugin(context.Background(), cfg.VerifierURL, cfg.JWTToken, cfg.PluginID, logger) + if err != nil { + logger.WithError(err).Warn("failed to delete existing plugin (may not exist)") + } sessionID := uuid.New().String() hexEncKey, err := generateHexEncryptionKey() if err != nil { - return fmt.Errorf("failed to generate encryption key: %w", err) + return nil, fmt.Errorf("failed to generate encryption key: %w", err) } relayClient := vgrelay.NewRelayClient(cfg.RelayURL) err = relayClient.RegisterSessionWithRetry(sessionID, vault.LocalPartyId) if err != nil { - return fmt.Errorf("failed to register with relay: %w", err) + return nil, fmt.Errorf("failed to register user party: %w", err) + } + err = relayClient.RegisterSessionWithRetry(sessionID, serverVault.LocalPartyId) + if err != nil { + return nil, fmt.Errorf("failed to register server party: %w", err) } - logger.WithField("session_id", sessionID).Info("registered with relay") + logger.WithField("session_id", sessionID).Info("registered both old parties with relay") err = initiateReshare(cfg, vault, sessionID, hexEncKey) if err != nil { - return fmt.Errorf("failed to initiate reshare: %w", err) + return nil, fmt.Errorf("failed to initiate reshare: %w", err) } logger.Info("reshare initiated on verifier") ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() - parties, err := waitForParties(ctx, relayClient, sessionID, 3, logger) + parties, err := waitForParties(ctx, relayClient, sessionID, 4, logger) if err != nil { - return fmt.Errorf("failed waiting for parties: %w", err) + return nil, fmt.Errorf("failed waiting for parties: %w", err) } logger.WithField("parties", parties).Info("all parties registered") err = relayClient.StartSession(sessionID, parties) if err != nil { - return fmt.Errorf("failed to start session: %w", err) + return nil, fmt.Errorf("failed to start session: %w", err) } logger.Info("session started") - localPartyID := vault.LocalPartyId - ourIndex := slices.Index(parties, localPartyID) - if ourIndex < 0 { - return fmt.Errorf("our party %s not found in registered parties %v", localPartyID, parties) - } - oldParties := []int{ourIndex} - var newParties []int - for i := range parties { - if i != ourIndex { - newParties = append(newParties, i) - } - } - threshold := int(math.Ceil(float64(len(parties))*2.0/3.0)) - 1 + allCommittee, oldIndices, newIndices := buildReshareCommittee(vault.Signers, parties) + threshold := int(math.Ceil(float64(len(allCommittee)) * 2.0 / 3.0)) logger.WithFields(logrus.Fields{ - "threshold": threshold, - "old_parties": oldParties, - "new_parties": newParties, - "our_index": ourIndex, - }).Info("starting ECDSA reshare") - - ecdsaPubkey, chainCode, err := reshareWithRetry( - vault, sessionID, hexEncKey, parties, vault.PublicKeyEcdsa, - false, localPartyID, threshold, oldParties, newParties, - cfg.RelayURL, logger, - ) + "threshold": threshold, + "all_committee": allCommittee, + "old_indices": oldIndices, + "new_indices": newIndices, + }).Info("starting MPC reshare") + + reshareResult, err := performReshare(vault, serverVault, sessionID, hexEncKey, allCommittee, threshold, oldIndices, newIndices, cfg.RelayURL, logger) if err != nil { - return fmt.Errorf("ECDSA reshare failed: %w", err) + return nil, fmt.Errorf("MPC reshare failed: %w", err) } - logger.WithField("ecdsa_pubkey", ecdsaPubkey).Info("ECDSA reshare completed") - logger.Info("starting EdDSA reshare") - eddsaPubkey, _, err := reshareWithRetry( - vault, sessionID, hexEncKey, parties, vault.PublicKeyEddsa, - true, localPartyID, threshold, oldParties, newParties, - cfg.RelayURL, logger, - ) - if err != nil { - return fmt.Errorf("EdDSA reshare failed: %w", err) + logger.WithFields(logrus.Fields{ + "ecdsa_keyshares": len(reshareResult.ECDSAKeyshares), + "eddsa_keyshares": len(reshareResult.EdDSAKeyshares), + }).Info("reshare completed, keyshares saved") + + for _, partyID := range []string{vault.LocalPartyId, serverVault.LocalPartyId} { + completeErr := relayClient.CompleteSession(sessionID, partyID) + if completeErr != nil { + logger.WithError(completeErr).WithField("party", partyID).Warn("failed to complete session") + } } - logger.WithField("eddsa_pubkey", eddsaPubkey).Info("EdDSA reshare completed") - err = relayClient.CompleteSession(sessionID, localPartyID) - if err != nil { - logger.WithError(err).Warn("failed to complete session (non-fatal)") + isCompleted, checkErr := relayClient.CheckCompletedParties(sessionID, parties) + if checkErr != nil || !isCompleted { + logger.WithFields(logrus.Fields{ + "is_completed": isCompleted, + "error": checkErr, + }).Warn("not all parties completed") } - _ = chainCode - logger.WithFields(logrus.Fields{ - "ecdsa_pubkey": ecdsaPubkey, - "eddsa_pubkey": eddsaPubkey, - }).Info("install completed successfully") + logger.Info("waiting 30s for plugin-worker to upload vault to storage") + time.Sleep(30 * time.Second) - return nil + logger.Info("install completed: MPC reshare succeeded") + return reshareResult, nil } -func parseVaultFromFixture(fixture *FixtureData, encryptionSecret string) (*vaultType.Vault, error) { - containerBytes, err := base64.StdEncoding.DecodeString(fixture.Vault.VaultB64) +func deleteExistingPlugin(ctx context.Context, verifierURL, jwtToken, pluginID string, logger *logrus.Logger) error { + url := verifierURL + "/plugin/" + pluginID + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) if err != nil { - return nil, fmt.Errorf("failed to decode vault_b64: %w", err) + return fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("Authorization", "Bearer "+jwtToken) - var container vaultType.VaultContainer - err = proto.Unmarshal(containerBytes, &container) + resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("failed to unmarshal vault container: %w", err) + return fmt.Errorf("request failed: %w", err) } + defer resp.Body.Close() - var vaultBytes []byte - if container.IsEncrypted { - if encryptionSecret == "" { - return nil, fmt.Errorf("vault is encrypted but no encryption secret provided") - } - encBytes, decErr := base64.StdEncoding.DecodeString(container.Vault) - if decErr != nil { - return nil, fmt.Errorf("failed to decode encrypted vault string: %w", decErr) - } - vaultBytes, err = vgcommon.DecryptVault(encryptionSecret, encBytes) - if err != nil { - return nil, fmt.Errorf("failed to decrypt vault: %w", err) - } - } else { - vaultBytes, err = base64.StdEncoding.DecodeString(container.Vault) - if err != nil { - return nil, fmt.Errorf("failed to decode vault string: %w", err) - } + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound { + logger.WithField("status", resp.StatusCode).Info("deleted existing plugin installation") + return nil } - var vault vaultType.Vault - err = proto.Unmarshal(vaultBytes, &vault) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal vault: %w", err) - } + var respBody bytes.Buffer + io.Copy(&respBody, io.LimitReader(resp.Body, maxErrorResponseBytes)) + return fmt.Errorf("delete plugin returned %d: %s", resp.StatusCode, respBody.String()) +} - if vault.LocalPartyId == "" { - return nil, fmt.Errorf("vault has empty local_party_id") +func buildReshareCommittee(oldSigners, relayParties []string) (allCommittee []string, oldIndices, newIndices []int) { + seen := make(map[string]bool) + relaySet := make(map[string]bool, len(relayParties)) + for _, p := range relayParties { + relaySet[p] = true } - if vault.PublicKeyEcdsa == "" { - return nil, fmt.Errorf("vault has empty public_key_ecdsa") + oldSet := make(map[string]bool, len(oldSigners)) + for _, s := range oldSigners { + oldSet[s] = true } - return &vault, nil -} - -func generateHexEncryptionKey() (string, error) { - key := make([]byte, 32) - _, err := rand.Read(key) - if err != nil { - return "", fmt.Errorf("failed to generate random key: %w", err) + for _, s := range oldSigners { + if !seen[s] { + allCommittee = append(allCommittee, s) + seen[s] = true + } } - return hex.EncodeToString(key), nil -} - -type reshareRequest struct { - Name string `json:"name"` - PublicKey string `json:"public_key"` - SessionID string `json:"session_id"` - HexEncryptionKey string `json:"hex_encryption_key"` - HexChainCode string `json:"hex_chain_code"` - LocalPartyId string `json:"local_party_id"` - OldParties []string `json:"old_parties"` - Email string `json:"email"` - PluginID string `json:"plugin_id"` -} - -func initiateReshare(cfg InstallConfig, vault *vaultType.Vault, sessionID string, hexEncKey string) error { - req := reshareRequest{ - Name: vault.Name, - PublicKey: vault.PublicKeyEcdsa, - SessionID: sessionID, - HexEncryptionKey: hexEncKey, - HexChainCode: vault.HexChainCode, - LocalPartyId: vault.LocalPartyId, - OldParties: vault.Signers, - Email: cfg.Fixture.Reshare.Email, - PluginID: cfg.PluginID, + for _, p := range relayParties { + if !seen[p] { + allCommittee = append(allCommittee, p) + seen[p] = true + } } - body, err := json.Marshal(req) - if err != nil { - return fmt.Errorf("failed to marshal reshare request: %w", err) + for i, party := range allCommittee { + if oldSet[party] { + oldIndices = append(oldIndices, i) + } + if relaySet[party] { + newIndices = append(newIndices, i) + } } - url := cfg.VerifierURL + "/vault/reshare" - httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("failed to create HTTP request: %w", err) - } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+cfg.JWTToken) + return allCommittee, oldIndices, newIndices +} - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(httpReq) - if err != nil { - return fmt.Errorf("reshare request failed: %w", err) +func performReshare( + vault *vaultType.Vault, + serverVault *vaultType.Vault, + sessionID string, + hexEncKey string, + allCommittee []string, + threshold int, + oldIndices []int, + newIndices []int, + relayURL string, + logger *logrus.Logger, +) (*ReshareResult, error) { + result := &ReshareResult{ + ECDSAKeyshares: map[string][]byte{}, + EdDSAKeyshares: map[string][]byte{}, } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - var respBody bytes.Buffer - io.Copy(&respBody, io.LimitReader(resp.Body, 4096)) - return fmt.Errorf("reshare request returned %d: %s", resp.StatusCode, respBody.String()) + curves := []struct { + label string + publicKey string + isEdDSA bool + }{ + {"ECDSA", vault.PublicKeyEcdsa, false}, + {"EdDSA", vault.PublicKeyEddsa, true}, } - return nil -} - -func waitForParties(ctx context.Context, client *vgrelay.Client, sessionID string, expectedCount int, logger *logrus.Logger) ([]string, error) { - for { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("timeout waiting for %d parties: %w", expectedCount, ctx.Err()) - default: - } + for _, curve := range curves { + logger.WithField("curve", curve.label).Info("starting reshare for curve") - parties, err := client.GetSession(sessionID) + keyshares, err := reshareWithRetry( + vault, serverVault, sessionID, hexEncKey, allCommittee, + threshold, oldIndices, newIndices, + curve.publicKey, curve.isEdDSA, curve.label, + relayURL, logger, + ) if err != nil { - logger.WithError(err).Debug("polling parties (will retry)") - time.Sleep(time.Second) - continue + return nil, fmt.Errorf("%s reshare failed: %w", curve.label, err) } - if len(parties) >= expectedCount { - return parties, nil + if curve.isEdDSA { + result.EdDSAKeyshares = keyshares + result.EdDSAPubKey = curve.publicKey + } else { + result.ECDSAKeyshares = keyshares + result.ECDSAPubKey = curve.publicKey } - logger.WithField("registered", len(parties)).Debug("waiting for more parties") - time.Sleep(time.Second) + logger.WithField("curve", curve.label).Info("reshare completed for curve") } + + result.HexChainCode = vault.HexChainCode + return result, nil } func reshareWithRetry( vault *vaultType.Vault, + serverVault *vaultType.Vault, sessionID string, hexEncKey string, - parties []string, + allCommittee []string, + threshold int, + oldIndices []int, + newIndices []int, publicKey string, isEdDSA bool, - localPartyID string, - threshold int, - oldParties []int, - newParties []int, + curveLabel string, relayURL string, logger *logrus.Logger, -) (string, string, error) { +) (map[string][]byte, error) { + var lastErr error for attempt := 0; attempt < 3; attempt++ { - pubkey, chainCode, err := reshare( - vault, sessionID, hexEncKey, parties, publicKey, - isEdDSA, localPartyID, threshold, oldParties, newParties, - relayURL, logger, attempt, + logger.WithFields(logrus.Fields{ + "curve": curveLabel, + "attempt": attempt, + }).Info("reshare attempt") + + keyshares, err := reshare( + vault, serverVault, sessionID, hexEncKey, allCommittee, + threshold, oldIndices, newIndices, + publicKey, isEdDSA, relayURL, logger, ) - if err == nil { - return pubkey, chainCode, nil + if err != nil { + lastErr = err + logger.WithError(err).WithField("attempt", attempt).Error("reshare attempt failed") + continue } - logger.WithError(err).WithField("attempt", attempt).Error("reshare attempt failed") + return keyshares, nil } - return "", "", fmt.Errorf("reshare failed after 3 attempts") + return nil, fmt.Errorf("reshare failed after 3 attempts: %w", lastErr) +} + +func loadKeyshareHandle(vault *vaultType.Vault, publicKey string, wrapper *mpc.Wrapper) (mpc.Handle, error) { + ks := findKeyshare(vault, publicKey) + if ks == "" { + return 0, fmt.Errorf("keyshare not found for public key %s in vault %s", publicKey, vault.LocalPartyId) + } + ksBytes, err := base64.StdEncoding.DecodeString(ks) + if err != nil { + return 0, fmt.Errorf("failed to decode keyshare: %w", err) + } + handle, err := wrapper.KeyshareFromBytes(ksBytes) + if err != nil { + return 0, fmt.Errorf("failed to load keyshare: %w", err) + } + return handle, nil } func reshare( vault *vaultType.Vault, + serverVault *vaultType.Vault, sessionID string, hexEncKey string, - parties []string, + allCommittee []string, + threshold int, + oldIndices []int, + newIndices []int, publicKey string, isEdDSA bool, - localPartyID string, - threshold int, - oldParties []int, - newParties []int, relayURL string, logger *logrus.Logger, - attempt int, -) (string, string, error) { - curveLabel := "ECDSA" - if isEdDSA { - curveLabel = "EdDSA" - } - logger.WithFields(logrus.Fields{ - "curve": curveLabel, - "attempt": attempt, - }).Info("reshare attempt") +) (map[string][]byte, error) { + userWrapper := mpc.NewWrapper(isEdDSA) + serverWrapper := mpc.NewWrapper(isEdDSA) - wrapper := mpc.NewWrapper(isEdDSA) + userKsHandle, err := loadKeyshareHandle(vault, publicKey, userWrapper) + if err != nil { + return nil, fmt.Errorf("user keyshare: %w", err) + } + defer userWrapper.KeyshareFree(userKsHandle) - var keyshareHandle mpc.Handle - if publicKey != "" { - keyshare := findKeyshare(vault, publicKey) - if keyshare == "" { - return "", "", fmt.Errorf("keyshare not found for public key %s", publicKey) - } - keyshareBytes, err := base64.StdEncoding.DecodeString(keyshare) - if err != nil { - return "", "", fmt.Errorf("failed to decode keyshare: %w", err) - } - keyshareHandle, err = wrapper.KeyshareFromBytes(keyshareBytes) - if err != nil { - return "", "", fmt.Errorf("failed to load keyshare: %w", err) - } - defer func() { - freeErr := wrapper.KeyshareFree(keyshareHandle) - if freeErr != nil { - logger.WithError(freeErr).Error("failed to free keyshare handle") - } - }() + serverKsHandle, err := loadKeyshareHandle(serverVault, publicKey, serverWrapper) + if err != nil { + return nil, fmt.Errorf("server keyshare: %w", err) } + defer serverWrapper.KeyshareFree(serverKsHandle) - setupMsg, err := wrapper.QcSetupMsgNew(keyshareHandle, threshold, parties, oldParties, newParties) + setupMsg, err := userWrapper.QcSetupMsgNew(userKsHandle, threshold, allCommittee, oldIndices, newIndices) if err != nil { - return "", "", fmt.Errorf("failed to create QC setup message: %w", err) + return nil, fmt.Errorf("QcSetupMsgNew failed: %w", err) } encrypted, err := mpc.EncryptEncodeSetupMessage(setupMsg, hexEncKey) if err != nil { - return "", "", fmt.Errorf("failed to encrypt setup message: %w", err) + return nil, fmt.Errorf("failed to encrypt setup message: %w", err) } - - relayClient := vgrelay.NewRelayClient(relayURL) setupMsgID := "" if isEdDSA { setupMsgID = "eddsa" } + relayClient := vgrelay.NewRelayClient(relayURL) err = relayClient.UploadSetupMessage(sessionID, setupMsgID, encrypted) if err != nil { - return "", "", fmt.Errorf("failed to upload setup message: %w", err) + return nil, fmt.Errorf("failed to upload setup message: %w", err) } - logger.Info("setup message uploaded") + logger.Info("QC setup message uploaded") - handle, err := wrapper.QcSessionFromSetup(setupMsg, localPartyID, keyshareHandle) + userPartyID := vault.LocalPartyId + serverPartyID := serverVault.LocalPartyId + + userHandle, err := userWrapper.QcSessionFromSetup(setupMsg, userPartyID, userKsHandle) if err != nil { - return "", "", fmt.Errorf("failed to create QC session from setup: %w", err) + return nil, fmt.Errorf("QcSessionFromSetup (user) failed: %w", err) } - err = processQcOutbound(handle, sessionID, hexEncKey, parties, localPartyID, isEdDSA, relayURL, logger) + serverHandle, err := serverWrapper.QcSessionFromSetup(setupMsg, serverPartyID, serverKsHandle) if err != nil { - logger.WithError(err).Error("initial outbound processing failed") + return nil, fmt.Errorf("QcSessionFromSetup (server) failed: %w", err) } - isInNewCommittee := slices.Contains(parties, localPartyID) + userLog := logger.WithField("role", "user") + serverLog := logger.WithField("role", "server") - newPubkey, chainCode, err := processQcInbound( - handle, wrapper, sessionID, hexEncKey, isEdDSA, - localPartyID, isInNewCommittee, parties, relayURL, logger, - ) - return newPubkey, chainCode, err -} + var wg sync.WaitGroup + var userErr, serverErr error + var userKsBytes, serverKsBytes []byte -func findKeyshare(vault *vaultType.Vault, publicKey string) string { - for _, ks := range vault.KeyShares { - if ks.PublicKey == publicKey { - return ks.Keyshare + wg.Add(2) + go func() { + defer wg.Done() + outErr := processQcOutbound(userHandle, sessionID, hexEncKey, allCommittee, userPartyID, isEdDSA, relayURL, userWrapper, userLog) + if outErr != nil { + userLog.WithError(outErr).Error("initial outbound failed") } + _, _, userKsBytes, userErr = processQcInbound( + userHandle, sessionID, hexEncKey, isEdDSA, userPartyID, + true, allCommittee, relayURL, userWrapper, userLog, + ) + }() + + go func() { + defer wg.Done() + outErr := processQcOutbound(serverHandle, sessionID, hexEncKey, allCommittee, serverPartyID, isEdDSA, relayURL, serverWrapper, serverLog) + if outErr != nil { + serverLog.WithError(outErr).Error("initial outbound failed") + } + serverIsInNew := slices.Contains(allCommittee, serverPartyID) + _, _, serverKsBytes, serverErr = processQcInbound( + serverHandle, sessionID, hexEncKey, isEdDSA, serverPartyID, + serverIsInNew, allCommittee, relayURL, serverWrapper, serverLog, + ) + }() + + wg.Wait() + + if userErr != nil { + return nil, fmt.Errorf("user party failed: %w", userErr) } - return "" + if serverErr != nil { + return nil, fmt.Errorf("server party failed: %w", serverErr) + } + + keyshares := map[string][]byte{} + if userKsBytes != nil { + keyshares[userPartyID] = userKsBytes + } + if serverKsBytes != nil { + keyshares[serverPartyID] = serverKsBytes + } + + logger.Info("both old parties completed reshare") + return keyshares, nil } func processQcOutbound( @@ -406,10 +448,10 @@ func processQcOutbound( localPartyID string, isEdDSA bool, relayURL string, - logger *logrus.Logger, + wrapper *mpc.Wrapper, + logger logrus.FieldLogger, ) error { - messenger := relay.NewMessenger(relayURL, sessionID, hexEncKey, true, "") - wrapper := mpc.NewWrapper(isEdDSA) + messenger := vsrelay.NewMessenger(relayURL, sessionID, hexEncKey, true, "") for { outbound, err := wrapper.QcSessionOutputMessage(handle) if err != nil { @@ -419,17 +461,18 @@ func processQcOutbound( return nil } encodedOutbound := base64.StdEncoding.EncodeToString(outbound) - for i := range parties { + for i := 0; i < len(parties); i++ { receiver, recvErr := wrapper.QcSessionMessageReceiver(handle, outbound, i) if recvErr != nil { - logger.WithError(recvErr).Error("failed to get message receiver") + logger.WithError(recvErr).Error("failed to get receiver") } - if receiver == "" { + if len(receiver) == 0 { break } + logger.WithField("receiver", receiver).Debug("sending message") sendErr := messenger.Send(localPartyID, receiver, encodedOutbound) if sendErr != nil { - logger.WithError(sendErr).Error("failed to send message") + logger.WithError(sendErr).Errorf("failed to send message to %s", receiver) } } } @@ -437,7 +480,6 @@ func processQcOutbound( func processQcInbound( handle mpc.Handle, - wrapper *mpc.Wrapper, sessionID string, hexEncKey string, isEdDSA bool, @@ -445,21 +487,23 @@ func processQcInbound( isInNewCommittee bool, parties []string, relayURL string, - logger *logrus.Logger, -) (string, string, error) { - processedFirstMsg := false - messageCache := make(map[string]bool) + wrapper *mpc.Wrapper, + logger logrus.FieldLogger, +) (string, string, []byte, error) { + var processedInitiatorMsg atomic.Bool + processedInitiatorMsg.Store(false) + var messageCache sync.Map relayClient := vgrelay.NewRelayClient(relayURL) start := time.Now() for { if time.Since(start) > 4*time.Minute { - return "", "", fmt.Errorf("reshare timed out after 4 minutes") + return "", "", nil, fmt.Errorf("reshare timed out after 4 minutes") } messages, err := relayClient.DownloadMessages(sessionID, localPartyID, "") if err != nil { - logger.WithError(err).Error("failed to download messages") + logger.WithError(err).Debug("failed to download messages (will retry)") time.Sleep(100 * time.Millisecond) continue } @@ -470,18 +514,19 @@ func processQcInbound( } cacheKey := fmt.Sprintf("%s-%s-%s", sessionID, localPartyID, message.Hash) - if messageCache[cacheKey] { + if _, found := messageCache.Load(cacheKey); found { continue } - if !processedFirstMsg && message.From != parties[0] { + if localPartyID != parties[0] && !processedInitiatorMsg.Load() && message.From != parties[0] { + logger.Debug("waiting for message from initiator party") continue } - processedFirstMsg = true + processedInitiatorMsg.Store(true) inboundBody, decErr := mpc.DecodeDecryptMessage(message.Body, hexEncKey) if decErr != nil { - logger.WithError(decErr).Error("failed to decode inbound message") + logger.WithError(decErr).Error("failed to decode message") continue } @@ -490,63 +535,211 @@ func processQcInbound( logger.WithError(inputErr).Error("failed to apply input message") continue } - messageCache[cacheKey] = true + + messageCache.Store(cacheKey, true) logger.WithFields(logrus.Fields{ "hash": message.Hash, "from": message.From, "seq": message.SequenceNo, - }).Debug("applied inbound message") + }).Info("applied inbound message") delErr := relayClient.DeleteMessageFromServer(sessionID, localPartyID, message.Hash, "") if delErr != nil { - logger.WithError(delErr).Error("failed to delete message from server") + logger.WithError(delErr).Error("failed to delete message") } time.Sleep(50 * time.Millisecond) - outErr := processQcOutbound(handle, sessionID, hexEncKey, parties, localPartyID, isEdDSA, relayURL, logger) + outErr := processQcOutbound(handle, sessionID, hexEncKey, parties, localPartyID, isEdDSA, relayURL, wrapper, logger) if outErr != nil { - logger.WithError(outErr).Error("failed to process outbound after input") + logger.WithError(outErr).Error("failed to process outbound after inbound") } if isFinished { logger.Info("reshare finished") - result, finErr := wrapper.QcSessionFinish(handle) - if finErr != nil { - return "", "", fmt.Errorf("failed to finish QC session: %w", finErr) + result, finishErr := wrapper.QcSessionFinish(handle) + if finishErr != nil { + return "", "", nil, fmt.Errorf("QcSessionFinish failed: %w", finishErr) } - defer func() { - freeErr := wrapper.KeyshareFree(result) - if freeErr != nil { - logger.WithError(freeErr).Error("failed to free result keyshare") - } - }() if !isInNewCommittee { logger.Info("reshare finished but not in new committee") - return "", "", nil + return "", "", nil, nil } - pubkeyBytes, pubErr := wrapper.KeysharePublicKey(result) + publicKeyBytes, pubErr := wrapper.KeysharePublicKey(result) if pubErr != nil { - return "", "", fmt.Errorf("failed to get public key: %w", pubErr) + return "", "", nil, fmt.Errorf("failed to get public key: %w", pubErr) } - encodedPubkey := hex.EncodeToString(pubkeyBytes) + encodedPublicKey := hex.EncodeToString(publicKeyBytes) chainCode := "" if !isEdDSA { chainCodeBytes, ccErr := wrapper.KeyshareChainCode(result) if ccErr != nil { - return "", "", fmt.Errorf("failed to get chain code: %w", ccErr) + return "", "", nil, fmt.Errorf("failed to get chain code: %w", ccErr) } chainCode = hex.EncodeToString(chainCodeBytes) } - return encodedPubkey, chainCode, nil + keyshareBytes, ksErr := wrapper.KeyshareToBytes(result) + if ksErr != nil { + return "", "", nil, fmt.Errorf("failed to serialize keyshare: %w", ksErr) + } + + freeErr := wrapper.KeyshareFree(result) + if freeErr != nil { + logger.WithError(freeErr).Error("failed to free result keyshare") + } + + return encodedPublicKey, chainCode, keyshareBytes, nil } } - time.Sleep(100 * time.Millisecond) } } + +func parseVaultFromB64(vaultB64 string, encryptionSecret string) (*vaultType.Vault, error) { + containerBytes, err := base64.StdEncoding.DecodeString(vaultB64) + if err != nil { + return nil, fmt.Errorf("failed to decode vault_b64: %w", err) + } + + var container vaultType.VaultContainer + err = proto.Unmarshal(containerBytes, &container) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal vault container: %w", err) + } + + var vaultBytes []byte + if container.IsEncrypted { + if encryptionSecret == "" { + return nil, fmt.Errorf("vault is encrypted but no encryption secret provided") + } + encBytes, decErr := base64.StdEncoding.DecodeString(container.Vault) + if decErr != nil { + return nil, fmt.Errorf("failed to decode encrypted vault string: %w", decErr) + } + vaultBytes, err = vgcommon.DecryptVault(encryptionSecret, encBytes) + if err != nil { + return nil, fmt.Errorf("failed to decrypt vault: %w", err) + } + } else { + vaultBytes, err = base64.StdEncoding.DecodeString(container.Vault) + if err != nil { + return nil, fmt.Errorf("failed to decode vault string: %w", err) + } + } + + var vault vaultType.Vault + err = proto.Unmarshal(vaultBytes, &vault) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal vault: %w", err) + } + + if vault.LocalPartyId == "" { + return nil, fmt.Errorf("vault has empty local_party_id") + } + if vault.PublicKeyEcdsa == "" { + return nil, fmt.Errorf("vault has empty public_key_ecdsa") + } + + return &vault, nil +} + +func generateHexEncryptionKey() (string, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return "", fmt.Errorf("failed to generate random key: %w", err) + } + return hex.EncodeToString(key), nil +} + +type reshareRequest struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` + SessionID string `json:"session_id"` + HexEncryptionKey string `json:"hex_encryption_key"` + HexChainCode string `json:"hex_chain_code"` + LocalPartyId string `json:"local_party_id"` + OldParties []string `json:"old_parties"` + Email string `json:"email"` + PluginID string `json:"plugin_id"` +} + +func initiateReshare(cfg InstallConfig, vault *vaultType.Vault, sessionID string, hexEncKey string) error { + req := reshareRequest{ + Name: vault.Name, + PublicKey: vault.PublicKeyEcdsa, + SessionID: sessionID, + HexEncryptionKey: hexEncKey, + HexChainCode: vault.HexChainCode, + LocalPartyId: vault.LocalPartyId, + OldParties: vault.Signers, + Email: cfg.Fixture.Reshare.Email, + PluginID: cfg.PluginID, + } + + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal reshare request: %w", err) + } + + url := cfg.VerifierURL + "/vault/reshare" + httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+cfg.JWTToken) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("reshare request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var respBody bytes.Buffer + io.Copy(&respBody, io.LimitReader(resp.Body, maxErrorResponseBytes)) + return fmt.Errorf("reshare request returned %d: %s", resp.StatusCode, respBody.String()) + } + + return nil +} + +func waitForParties(ctx context.Context, client *vgrelay.Client, sessionID string, expectedCount int, logger *logrus.Logger) ([]string, error) { + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for %d parties: %w", expectedCount, ctx.Err()) + default: + } + + parties, err := client.GetSession(sessionID) + if err != nil { + logger.WithError(err).Debug("polling parties (will retry)") + time.Sleep(time.Second) + continue + } + + if len(parties) >= expectedCount { + return parties, nil + } + + logger.WithField("registered", len(parties)).Debug("waiting for more parties") + time.Sleep(time.Second) + } +} + +func findKeyshare(vault *vaultType.Vault, publicKey string) string { + for _, ks := range vault.KeyShares { + if ks.PublicKey == publicKey { + return ks.Keyshare + } + } + return "" +} diff --git a/internal/testrunner/policy.go b/internal/testrunner/policy.go new file mode 100644 index 0000000..9e28100 --- /dev/null +++ b/internal/testrunner/policy.go @@ -0,0 +1,476 @@ +package testrunner + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" + recipetypes "github.com/vultisig/recipes/types" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/vultisig/plugin-tests/internal/testrunner/mpc" +) + +const ethDerivePath = "m/44/60/0/0/0" + +type SuggestResult struct { + Recipe string + Resource string +} + +func RunPolicyCRUD(cfg InstallConfig, reshareResult *ReshareResult, logger *logrus.Logger) error { + vault, err := parseVaultFromB64(cfg.Fixture.Vault.VaultB64, cfg.EncryptionSecret) + if err != nil { + return fmt.Errorf("failed to parse user vault: %w", err) + } + serverVault, err := parseVaultFromB64(cfg.Fixture.Vault.ServerVaultB64, cfg.EncryptionSecret) + if err != nil { + return fmt.Errorf("failed to parse server vault: %w", err) + } + + client := NewTestClient(cfg.VerifierURL) + + suggestResult, err := buildRecipeFromSuggest(client, cfg.PluginID, cfg.TestTargetAddress, logger) + if err != nil { + return fmt.Errorf("failed to build recipe from suggest: %w", err) + } + logger.WithField("resource", suggestResult.Resource).Info("built recipe from suggest endpoint") + + policyVersion := 1 + pluginVersion := "1.0.0" + policyMsg := buildPolicyMessage(suggestResult.Recipe, vault.PublicKeyEcdsa, policyVersion, pluginVersion) + + signature, err := signPolicyMessage(vault, serverVault, policyMsg, logger) + if err != nil { + return fmt.Errorf("failed to sign policy message: %w", err) + } + logger.Info("policy message signed locally") + + policyID := uuid.New().String() + + err = createPolicy(client, cfg.JWTToken, policyID, vault.PublicKeyEcdsa, cfg.PluginID, pluginVersion, policyVersion, signature, suggestResult.Recipe) + if err != nil { + return fmt.Errorf("policy create failed: %w", err) + } + logger.WithField("policy_id", policyID).Info("policy created") + + err = getPolicy(client, cfg.JWTToken, policyID) + if err != nil { + return fmt.Errorf("policy read failed: %w", err) + } + logger.Info("policy read verified") + + deleteMsg := buildPolicyMessage(suggestResult.Recipe, vault.PublicKeyEcdsa, policyVersion, pluginVersion) + deleteSig, err := signPolicyMessage(vault, serverVault, deleteMsg, logger) + if err != nil { + return fmt.Errorf("failed to sign delete message: %w", err) + } + + err = deletePolicy(client, cfg.JWTToken, policyID, deleteSig) + if err != nil { + return fmt.Errorf("policy delete failed: %w", err) + } + logger.Info("policy deleted") + + return nil +} + +func buildRecipeFromSuggest(client *TestClient, pluginID, targetAddr string, logger *logrus.Logger) (*SuggestResult, error) { + resp, err := client.GET("/plugins/" + pluginID + "/recipe-specification") + if err != nil { + return nil, fmt.Errorf("failed to fetch recipe spec: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("recipe spec returned HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read spec response: %w", err) + } + + var specResp struct { + Data struct { + PluginID string `json:"plugin_id"` + ConfigurationExample []map[string]interface{} `json:"configuration_example"` + } `json:"data"` + } + err = json.Unmarshal(body, &specResp) + if err != nil { + return nil, fmt.Errorf("failed to parse spec response: %w", err) + } + + if len(specResp.Data.ConfigurationExample) == 0 { + return nil, fmt.Errorf("plugin %s has no configuration examples", pluginID) + } + + config := specResp.Data.ConfigurationExample[0] + config = fillEmptyAddresses(config, targetAddr).(map[string]interface{}) + logger.WithField("config", config).Info("suggest config prepared") + + suggestBody := map[string]interface{}{ + "configuration": config, + } + suggestResp, err := client.POST("/plugins/"+pluginID+"/recipe-specification/suggest", suggestBody) + if err != nil { + return nil, fmt.Errorf("suggest request failed: %w", err) + } + defer suggestResp.Body.Close() + + if suggestResp.StatusCode != http.StatusOK { + errBody, _ := io.ReadAll(suggestResp.Body) + return nil, fmt.Errorf("suggest returned HTTP %d: %s", suggestResp.StatusCode, string(errBody)) + } + + suggestRaw, err := io.ReadAll(suggestResp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read suggest response: %w", err) + } + logger.WithField("response", string(suggestRaw)).Info("suggest response received") + + var suggestEnvelope struct { + Data json.RawMessage `json:"data"` + } + err = json.Unmarshal(suggestRaw, &suggestEnvelope) + if err != nil { + return nil, fmt.Errorf("failed to parse suggest envelope: %w", err) + } + + var policySuggest recipetypes.PolicySuggest + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + err = unmarshaler.Unmarshal(suggestEnvelope.Data, &policySuggest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal PolicySuggest: %w", err) + } + + for i, rule := range policySuggest.Rules { + if rule.Id == "" { + rule.Id = fmt.Sprintf("rule-%d", i+1) + } + } + + configStruct, err := structpb.NewStruct(config) + if err != nil { + return nil, fmt.Errorf("failed to build configuration struct: %w", err) + } + + policy := &recipetypes.Policy{ + Id: pluginID, + Name: "Integration Test Policy", + Version: 1, + Author: "plugin-tests", + Rules: policySuggest.Rules, + RateLimitWindow: policySuggest.RateLimitWindow, + MaxTxsPerWindow: policySuggest.MaxTxsPerWindow, + Configuration: configStruct, + } + + data, err := proto.Marshal(policy) + if err != nil { + return nil, fmt.Errorf("failed to marshal policy: %w", err) + } + recipe := base64.StdEncoding.EncodeToString(data) + + result := &SuggestResult{ + Recipe: recipe, + } + + if len(policySuggest.Rules) > 0 { + result.Resource = policySuggest.Rules[0].Resource + } + + return result, nil +} + +func fillEmptyAddresses(v interface{}, addr string) interface{} { + switch val := v.(type) { + case map[string]interface{}: + result := make(map[string]interface{}, len(val)) + for k, inner := range val { + result[k] = fillEmptyAddresses(inner, addr) + } + return result + case []interface{}: + result := make([]interface{}, len(val)) + for i, inner := range val { + result[i] = fillEmptyAddresses(inner, addr) + } + return result + case string: + if val == "" { + return addr + } + return val + default: + return val + } +} + +func buildPolicyMessage(recipe, publicKey string, policyVersion int, pluginVersion string) []byte { + fields := []string{ + recipe, + publicKey, + strconv.Itoa(policyVersion), + pluginVersion, + } + return []byte(strings.Join(fields, "*#*")) +} + +func personalSignHash(message []byte) []byte { + prefixed := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message) + return crypto.Keccak256([]byte(prefixed)) +} + +func signPolicyMessage(vault, serverVault *vaultType.Vault, message []byte, logger *logrus.Logger) (string, error) { + wrapper := mpc.NewWrapper(false) + + userKsHandle, err := loadKeyshareHandle(vault, vault.PublicKeyEcdsa, wrapper) + if err != nil { + return "", fmt.Errorf("user keyshare: %w", err) + } + defer wrapper.KeyshareFree(userKsHandle) + + serverKsHandle, err := loadKeyshareHandle(serverVault, serverVault.PublicKeyEcdsa, wrapper) + if err != nil { + return "", fmt.Errorf("server keyshare: %w", err) + } + defer wrapper.KeyshareFree(serverKsHandle) + + hash := personalSignHash(message) + + sig, err := localSign( + userKsHandle, serverKsHandle, + vault.LocalPartyId, serverVault.LocalPartyId, + hash, []byte(ethDerivePath), + wrapper, + logger, + ) + if err != nil { + return "", fmt.Errorf("local sign failed: %w", err) + } + + if len(sig) == 65 && sig[64] < 27 { + sig[64] += 27 + } + + logger.Info("policy signature produced") + + return "0x" + hex.EncodeToString(sig), nil +} + +func localSign( + userKsHandle, serverKsHandle mpc.Handle, + userPartyID, serverPartyID string, + messageHash []byte, + chainPath []byte, + wrapper *mpc.Wrapper, + logger *logrus.Logger, +) ([]byte, error) { + keyID, err := wrapper.KeyshareKeyID(userKsHandle) + if err != nil { + return nil, fmt.Errorf("failed to get key ID: %w", err) + } + + ids := []byte(userPartyID + "\x00" + serverPartyID) + + setupMsg, err := wrapper.SignSetupMsgNew(keyID, chainPath, messageHash, ids) + if err != nil { + return nil, fmt.Errorf("SignSetupMsgNew failed: %w", err) + } + + userSession, err := wrapper.SignSessionFromSetup(setupMsg, []byte(userPartyID), userKsHandle) + if err != nil { + return nil, fmt.Errorf("user SignSessionFromSetup failed: %w", err) + } + defer wrapper.SignSessionFree(userSession) + + serverSession, err := wrapper.SignSessionFromSetup(setupMsg, []byte(serverPartyID), serverKsHandle) + if err != nil { + return nil, fmt.Errorf("server SignSessionFromSetup failed: %w", err) + } + defer wrapper.SignSessionFree(serverSession) + + type msgEnvelope struct { + data []byte + receiver string + } + + userToServer := make(chan msgEnvelope, 100) + serverToUser := make(chan msgEnvelope, 100) + + sendOutbound := func(session mpc.Handle, localID string, outCh chan<- msgEnvelope) { + for { + out, outErr := wrapper.SignSessionOutputMessage(session) + if outErr != nil || len(out) == 0 { + return + } + for i := 0; i < 2; i++ { + recv, recvErr := wrapper.SignSessionMessageReceiver(session, out, i) + if recvErr != nil || recv == "" { + break + } + if recv != localID { + outCh <- msgEnvelope{data: append([]byte(nil), out...), receiver: recv} + } + } + } + } + + var userSig, serverSig []byte + var userErr, serverErr error + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + sendOutbound(userSession, userPartyID, userToServer) + + for { + select { + case msg := <-serverToUser: + finished, inputErr := wrapper.SignSessionInputMessage(userSession, msg.data) + if inputErr != nil { + userErr = fmt.Errorf("user input failed: %w", inputErr) + return + } + sendOutbound(userSession, userPartyID, userToServer) + if finished { + userSig, userErr = wrapper.SignSessionFinish(userSession) + return + } + case <-time.After(30 * time.Second): + userErr = fmt.Errorf("user sign timed out") + return + } + } + }() + + go func() { + defer wg.Done() + sendOutbound(serverSession, serverPartyID, serverToUser) + + for { + select { + case msg := <-userToServer: + finished, inputErr := wrapper.SignSessionInputMessage(serverSession, msg.data) + if inputErr != nil { + serverErr = fmt.Errorf("server input failed: %w", inputErr) + return + } + sendOutbound(serverSession, serverPartyID, serverToUser) + if finished { + serverSig, serverErr = wrapper.SignSessionFinish(serverSession) + return + } + case <-time.After(30 * time.Second): + serverErr = fmt.Errorf("server sign timed out") + return + } + } + }() + + wg.Wait() + + if userErr != nil { + return nil, fmt.Errorf("user: %w", userErr) + } + if serverErr != nil { + return nil, fmt.Errorf("server: %w", serverErr) + } + + if len(userSig) > 0 { + return userSig, nil + } + return serverSig, nil +} + +func createPolicy(client *TestClient, jwtToken, policyID, publicKey, pluginID, pluginVersion string, policyVersion int, signature, recipe string) error { + body := map[string]interface{}{ + "id": policyID, + "public_key": publicKey, + "plugin_id": pluginID, + "plugin_version": pluginVersion, + "policy_version": policyVersion, + "signature": signature, + "recipe": recipe, + "active": true, + "billing": []interface{}{}, + } + + resp, err := client.WithJWT(jwtToken).POST("/plugin/policy", body) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return readErrorResponse(resp) + } + return nil +} + +func getPolicy(client *TestClient, jwtToken, policyID string) error { + resp, err := client.WithJWT(jwtToken).GET("/plugin/policy/" + policyID) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return readErrorResponse(resp) + } + + var apiResp struct { + Data struct { + ID string `json:"id"` + } `json:"data"` + } + err = ReadJSONResponse(resp, &apiResp) + if err != nil { + return err + } + if apiResp.Data.ID != policyID { + return fmt.Errorf("expected policy ID %s, got %s", policyID, apiResp.Data.ID) + } + return nil +} + +func deletePolicy(client *TestClient, jwtToken, policyID, signature string) error { + body := map[string]interface{}{ + "signature": signature, + } + + resp, err := client.WithJWT(jwtToken).DELETE("/plugin/policy/"+policyID, body) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return readErrorResponse(resp) + } + return nil +} + +func readErrorResponse(resp *http.Response) error { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("HTTP %d (could not read body)", resp.StatusCode) + } + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes)) +} diff --git a/internal/testrunner/seeder.go b/internal/testrunner/seeder.go index 9b55814..22a8a6a 100644 --- a/internal/testrunner/seeder.go +++ b/internal/testrunner/seeder.go @@ -1,25 +1,18 @@ package testrunner import ( - "bytes" "context" - "encoding/base64" "fmt" "time" + "github.com/jackc/pgx/v5/pgtype" "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" - vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" - recipetypes "github.com/vultisig/recipes/types" - "github.com/vultisig/vultisig-go/common" - "google.golang.org/protobuf/proto" "github.com/vultisig/plugin-tests/internal/testrunner/db" ) @@ -88,7 +81,7 @@ func (s *Seeder) SeedDatabase(ctx context.Context) error { return fmt.Errorf("failed to insert plugin %s: %w", plugin.ID, err) } - apiKey := fmt.Sprintf("integration-test-apikey-%s", plugin.ID) + apiKey := plugin.APIKey err = queries.UpsertPluginAPIKey(ctx, &db.UpsertPluginAPIKeyParams{ PluginID: plugin.ID, Apikey: apiKey, @@ -101,63 +94,19 @@ func (s *Seeder) SeedDatabase(ctx context.Context) error { s.logger.WithField("plugin_id", plugin.ID).Info("plugin seeded") } - vaultPubkey := s.config.Fixture.Vault.PublicKey - if vaultPubkey != "" { - tokenID := "integration-token-1" - now := time.Now() - expiresAt := now.Add(365 * 24 * time.Hour) - - s.logger.Info("inserting vault token") - - err = queries.UpsertVaultToken(ctx, &db.UpsertVaultTokenParams{ - TokenID: tokenID, - PublicKey: vaultPubkey, - ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true}, - LastUsedAt: pgtype.Timestamptz{Time: now, Valid: true}, - }) - if err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("failed to insert vault token: %w", err) - } - - s.logger.Info("inserting test policies") - - targetAddr := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" - permissiveRecipe, recipeErr := generatePermissivePolicy(targetAddr) - if recipeErr != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("failed to generate permissive policy: %w", recipeErr) - } - - for i, plugin := range s.config.Plugins { - policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) - parsed, parseErr := uuid.Parse(policyID) - if parseErr != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("failed to parse policy UUID %s: %w", policyID, parseErr) - } - - err = queries.UpsertPluginPolicy(ctx, &db.UpsertPluginPolicyParams{ - ID: pgtype.UUID{Bytes: parsed, Valid: true}, - PublicKey: vaultPubkey, - PluginID: plugin.ID, - PluginVersion: "1.0.0", - PolicyVersion: 1, - Signature: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - Recipe: permissiveRecipe, - Active: true, - }) - if err != nil { - _ = tx.Rollback(ctx) - return fmt.Errorf("failed to insert policy for plugin %s: %w", plugin.ID, err) - } - - s.logger.WithFields(logrus.Fields{ - "policy_id": policyID, - "plugin_id": plugin.ID, - }).Info("policy seeded") - } + now := time.Now() + tokenExpiry := now.Add(24 * time.Hour) + err = queries.UpsertVaultToken(ctx, &db.UpsertVaultTokenParams{ + TokenID: "integration-token-1", + PublicKey: s.config.Fixture.Vault.PublicKey, + ExpiresAt: pgtype.Timestamptz{Time: tokenExpiry, Valid: true}, + LastUsedAt: pgtype.Timestamptz{Time: now, Valid: true}, + }) + if err != nil { + _ = tx.Rollback(ctx) + return fmt.Errorf("failed to insert vault token: %w", err) } + s.logger.Info("vault token seeded") err = tx.Commit(ctx) if err != nil { @@ -169,29 +118,6 @@ func (s *Seeder) SeedDatabase(ctx context.Context) error { } func (s *Seeder) SeedVaults(ctx context.Context) error { - vaultData, err := base64.StdEncoding.DecodeString(s.config.Fixture.Vault.VaultB64) - if err != nil { - return fmt.Errorf("failed to decode vault_b64: %w", err) - } - - encryptedVault, err := common.EncryptVault(s.config.EncryptionSecret, vaultData) - if err != nil { - return fmt.Errorf("failed to encrypt vault: %w", err) - } - - vaultContainer := &vaultType.VaultContainer{ - Version: 1, - Vault: base64.StdEncoding.EncodeToString(encryptedVault), - IsEncrypted: true, - } - - containerBytes, err := proto.Marshal(vaultContainer) - if err != nil { - return fmt.Errorf("failed to marshal vault container: %w", err) - } - - vaultBackup := []byte(base64.StdEncoding.EncodeToString(containerBytes)) - sess, err := session.NewSession(&aws.Config{ Endpoint: aws.String(s.config.S3.Endpoint), Region: aws.String(s.config.S3.Region), @@ -211,99 +137,13 @@ func (s *Seeder) SeedVaults(ctx context.Context) error { s.logger.WithError(err).Debug("bucket creation (may already exist)") } - s.logger.Info("seeding vault fixtures to MinIO") - - for _, plugin := range s.config.Plugins { - key := fmt.Sprintf("%s-%s.vult", plugin.ID, s.config.Fixture.Vault.PublicKey) - - _, err = s3Client.PutObject(&s3.PutObjectInput{ - Bucket: aws.String(s.config.S3.Bucket), - Key: aws.String(key), - Body: bytes.NewReader(vaultBackup), - ContentType: aws.String("application/octet-stream"), - }) - if err != nil { - return fmt.Errorf("failed to upload %s: %w", key, err) - } - - s.logger.WithField("key", key).Info("uploaded vault file") - } - - billingKey := fmt.Sprintf("vultisig-fees-feee-%s.vult", s.config.Fixture.Vault.PublicKey) - _, err = s3Client.PutObject(&s3.PutObjectInput{ - Bucket: aws.String(s.config.S3.Bucket), - Key: aws.String(billingKey), - Body: bytes.NewReader(vaultBackup), - ContentType: aws.String("application/octet-stream"), + _, err = s3Client.CreateBucket(&s3.CreateBucketInput{ + Bucket: aws.String("vultisig-dca"), }) if err != nil { - return fmt.Errorf("failed to upload billing vault %s: %w", billingKey, err) + s.logger.WithError(err).Debug("vultisig-dca bucket creation (may already exist)") } - s.logger.WithField("key", billingKey).Info("uploaded billing vault") - s.logger.Info("vault seeding completed") + s.logger.Info("vault seeding completed (buckets created, no vault files uploaded for new-party reshare)") return nil } - -func generatePermissivePolicy(targetAddr string) (string, error) { - policy := &recipetypes.Policy{ - Id: "permissive-test-policy", - Name: "Permissive Test Policy", - Description: "Allows all transactions for testing", - Version: 1, - Author: "integration-test", - Rules: []*recipetypes.Rule{ - { - Id: "allow-ethereum-eth-transfer", - Resource: "ethereum.eth.transfer", - Effect: recipetypes.Effect_EFFECT_ALLOW, - Description: "Allow Ethereum transfers", - Target: &recipetypes.Target{ - TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, - Target: &recipetypes.Target_Address{ - Address: targetAddr, - }, - }, - ParameterConstraints: []*recipetypes.ParameterConstraint{ - { - ParameterName: "amount", - Constraint: &recipetypes.Constraint{ - Type: recipetypes.ConstraintType_CONSTRAINT_TYPE_ANY, - }, - }, - }, - }, - { - Id: "allow-ethereum-erc20-transfer", - Resource: "ethereum.erc20.transfer", - Effect: recipetypes.Effect_EFFECT_ALLOW, - Description: "Allow ERC20 transfers", - Target: &recipetypes.Target{ - TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, - Target: &recipetypes.Target_Address{ - Address: targetAddr, - }, - }, - }, - { - Id: "allow-ethereum-erc20-approve", - Resource: "ethereum.erc20.approve", - Effect: recipetypes.Effect_EFFECT_ALLOW, - Description: "Allow ERC20 approvals", - Target: &recipetypes.Target{ - TargetType: recipetypes.TargetType_TARGET_TYPE_ADDRESS, - Target: &recipetypes.Target_Address{ - Address: targetAddr, - }, - }, - }, - }, - } - - data, err := proto.Marshal(policy) - if err != nil { - return "", fmt.Errorf("failed to marshal policy: %w", err) - } - - return base64.StdEncoding.EncodeToString(data), nil -} diff --git a/internal/testrunner/tests.go b/internal/testrunner/tests.go index f98c80f..3cb8014 100644 --- a/internal/testrunner/tests.go +++ b/internal/testrunner/tests.go @@ -8,28 +8,26 @@ import ( ) type TestSuite struct { - client *TestClient - pluginCli *TestClient - fixture *FixtureData - plugins []PluginConfig - jwtToken string - evmFixture *EVMFixture - logger *logrus.Logger - Passed int - Failed int - Total int - Errors []string + client *TestClient + pluginCli *TestClient + fixture *FixtureData + plugins []PluginConfig + jwtToken string + logger *logrus.Logger + Passed int + Failed int + Total int + Errors []string } -func NewTestSuite(client *TestClient, pluginCli *TestClient, fixture *FixtureData, plugins []PluginConfig, jwtToken string, evmFixture *EVMFixture, logger *logrus.Logger) *TestSuite { +func NewTestSuite(client *TestClient, pluginCli *TestClient, fixture *FixtureData, plugins []PluginConfig, jwtToken string, logger *logrus.Logger) *TestSuite { return &TestSuite{ - client: client, - pluginCli: pluginCli, - fixture: fixture, - plugins: plugins, - jwtToken: jwtToken, - evmFixture: evmFixture, - logger: logger, + client: client, + pluginCli: pluginCli, + fixture: fixture, + plugins: plugins, + jwtToken: jwtToken, + logger: logger, } } @@ -149,10 +147,8 @@ func (s *TestSuite) healthChecks() bool { func (s *TestSuite) verifierPluginTests() bool { beforeFailed := s.Failed - for i, plugin := range s.plugins { + for _, plugin := range s.plugins { pluginID := plugin.ID - apiKey := fmt.Sprintf("integration-test-apikey-%s", pluginID) - policyID := fmt.Sprintf("00000000-0000-0000-0000-0000000000%02d", i+11) s.run(pluginID+"/GetRecipeSpecification", func() error { resp, err := s.client.GET("/plugins/" + pluginID + "/recipe-specification") @@ -221,56 +217,12 @@ func (s *TestSuite) verifierPluginTests() bool { return fmt.Errorf("request failed (verifier->plugin connectivity): %w", err) } defer resp.Body.Close() - return nil - }) - - s.run(pluginID+"/Sign", func() error { - reqBody := map[string]interface{}{ - "plugin_id": pluginID, - "public_key": s.fixture.Vault.PublicKey, - "policy_id": policyID, - "transactions": s.evmFixture.TxB64, - "transaction_type": "evm", - "messages": []map[string]interface{}{ - { - "message": s.evmFixture.MsgB64, - "chain": "Ethereum", - "hash": s.evmFixture.MsgSHA256B64, - "hash_function": "SHA256", - }, - }, - } - - resp, err := s.client.WithAPIKey(apiKey).POST("/plugin-signer/sign", reqBody) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected 200, got %d", resp.StatusCode) + if resp.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("reshare returned 401 unauthorized (JWT token_type issue?)") } - - var apiResp struct { - Data struct { - TaskIDs []string `json:"task_ids"` - } `json:"data"` - } - err = ReadJSONResponse(resp, &apiResp) - if err != nil { - return err - } - if len(apiResp.Data.TaskIDs) == 0 { - return fmt.Errorf("expected at least 1 task_id") - } - - taskID := apiResp.Data.TaskIDs[0] - pollResp, pollErr := s.client.WithAPIKey(apiKey).GET("/plugin-signer/sign/response/" + taskID) - if pollErr != nil { - return fmt.Errorf("sign response poll failed: %w", pollErr) - } - defer pollResp.Body.Close() return nil }) + } return s.Failed == beforeFailed diff --git a/internal/worker/artifacts.go b/internal/worker/artifacts.go index 8be32fb..486bcef 100644 --- a/internal/worker/artifacts.go +++ b/internal/worker/artifacts.go @@ -53,6 +53,13 @@ func (u *ArtifactUploader) UploadRunArtifacts(ctx context.Context, runID string, } } + if result.InstallLogs != "" { + err = u.upload(ctx, client, prefix+"/install.txt", result.InstallLogs) + if err != nil { + return prefix, fmt.Errorf("failed to upload install logs: %w", err) + } + } + return prefix, nil } diff --git a/internal/worker/consumer.go b/internal/worker/consumer.go index c70e3fb..aac4b98 100644 --- a/internal/worker/consumer.go +++ b/internal/worker/consumer.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "time" "github.com/google/uuid" "github.com/hibiken/asynq" @@ -71,14 +70,16 @@ func (c *Consumer) Handle(ctx context.Context, t *asynq.Task) error { if !createdNS { return } - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - delErr := deleteNamespace(cleanupCtx, c.k8s, nsName) - if delErr != nil { - log.WithError(delErr).Warn("failed to cleanup namespace") - } else { - log.Info("namespace cleaned up") - } + // TODO: re-enable after debugging + // cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // defer cancel() + // delErr := deleteNamespace(cleanupCtx, c.k8s, nsName) + // if delErr != nil { + // log.WithError(delErr).Warn("failed to cleanup namespace") + // } else { + // log.Info("namespace cleaned up") + // } + log.Info("namespace cleanup skipped (debug mode)") }() err = createNamespace(ctx, c.k8s, nsName, labels) @@ -89,7 +90,28 @@ func (c *Consumer) Handle(ctx context.Context, t *asynq.Task) error { createdNS = true log.Info("namespace created") - runner := NewRunner(c.k8s, c.cfg, log) + if c.cfg.ImagePullSecret != "" { + copyErr := copySecret(ctx, c.k8s, c.cfg.SystemNamespace, nsName, c.cfg.ImagePullSecret) + if copyErr != nil { + log.WithError(copyErr).Warn("failed to copy image pull secret") + } + } + if c.cfg.TLSSecretName != "" { + copyErr := copySecret(ctx, c.k8s, c.cfg.SystemNamespace, nsName, c.cfg.TLSSecretName) + if copyErr != nil { + log.WithError(copyErr).Warn("failed to copy TLS secret") + } + } + + jobCfg := c.cfg + if payload.PluginEndpoint != "" { + jobCfg.PluginEndpoint = payload.PluginEndpoint + } + if payload.PluginAPIKey != "" { + jobCfg.PluginAPIKey = payload.PluginAPIKey + } + + runner := NewRunner(c.k8s, jobCfg, log) result := runner.Run(ctx, nsName, payload.RunID, payload.PluginID, labels) uploader := NewArtifactUploader(c.artifactCfg) diff --git a/internal/worker/k8s.go b/internal/worker/k8s.go index 26e1a46..fa6e1ae 100644 --- a/internal/worker/k8s.go +++ b/internal/worker/k8s.go @@ -39,6 +39,29 @@ func createNamespace(ctx context.Context, clientset kubernetes.Interface, name s return nil } +func copySecret(ctx context.Context, clientset kubernetes.Interface, srcNS, dstNS, name string) error { + secret, err := clientset.CoreV1().Secrets(srcNS).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("get secret %s/%s: %w", srcNS, name, err) + } + copy := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name, + Namespace: dstNS, + }, + Type: secret.Type, + Data: secret.Data, + } + _, err = clientset.CoreV1().Secrets(dstNS).Create(ctx, copy, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("create secret %s/%s: %w", dstNS, name, err) + } + return nil +} + func createDenyAllNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string) error { dnsPort := intstr.FromInt32(53) udp := corev1.ProtocolUDP @@ -255,6 +278,61 @@ func createIntraNamespaceNetworkPolicy(ctx context.Context, clientset kubernetes return nil } +func applyIngress(ctx context.Context, clientset kubernetes.Interface, ing *networkingv1.Ingress) error { + _, err := clientset.NetworkingV1().Ingresses(ing.Namespace).Create(ctx, ing, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create ingress %s: %w", ing.Name, err) + } + return nil +} + +func createIngressNetworkPolicy(ctx context.Context, clientset kubernetes.Interface, namespace string) error { + tcp := corev1.ProtocolTCP + verifierPort := intstr.FromInt32(8080) + + policy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-ingress-controller", + Namespace: namespace, + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "verifier"}, + }, + PolicyTypes: []networkingv1.PolicyType{ + networkingv1.PolicyTypeIngress, + }, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "ingress-nginx", + }, + }, + }, + }, + Ports: []networkingv1.NetworkPolicyPort{ + {Protocol: &tcp, Port: &verifierPort}, + }, + }, + }, + }, + } + _, err := clientset.NetworkingV1().NetworkPolicies(namespace).Create(ctx, policy, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create ingress network policy in %s: %w", namespace, err) + } + return nil +} + func applyDeployment(ctx context.Context, clientset kubernetes.Interface, dep *appsv1.Deployment) error { _, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) if err != nil { diff --git a/internal/worker/manifests.go b/internal/worker/manifests.go index 701f4e6..864da25 100644 --- a/internal/worker/manifests.go +++ b/internal/worker/manifests.go @@ -10,6 +10,7 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -352,6 +353,62 @@ func verifierDeploymentObjects(ns, image, pullSecret string, labels map[string]s return dep, svc } +func verifierIngress(ns, host, tlsSecretName string, labels map[string]string) *networkingv1.Ingress { + pathType := networkingv1.PathTypePrefix + ingressClassName := "nginx" + + annotations := map[string]string{} + if tlsSecretName == "" { + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + } + + ing := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "verifier", + Namespace: ns, + Labels: labels, + Annotations: annotations, + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: &ingressClassName, + Rules: []networkingv1.IngressRule{ + { + Host: host, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "verifier", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + if tlsSecretName != "" { + ing.Spec.TLS = []networkingv1.IngressTLS{ + { + Hosts: []string{host}, + SecretName: tlsSecretName, + }, + } + } + + return ing +} + func verifierWorkerDeployment(ns, image, pullSecret string, labels map[string]string) *appsv1.Deployment { selectorLabels := map[string]string{"app": "verifier-worker"} allLabels := mergeLabels(labels, selectorLabels) @@ -404,6 +461,10 @@ func testJob(ns, image, pullSecret string, labels map[string]string, envVars []c return buildTestJob("test", ns, image, pullSecret, labels, []string{"test"}, envVars, ttlSeconds, hostAliases) } +func installJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { + return buildTestJob("install", ns, image, pullSecret, labels, []string{"install"}, envVars, ttlSeconds, hostAliases) +} + func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, args []string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { var backoffLimit int32 var ttlPtr *int32 @@ -447,7 +508,7 @@ func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, } func testrunnerEnvVars(cfg config.K8sJobConfig) []corev1.EnvVar { - return []corev1.EnvVar{ + vars := []corev1.EnvVar{ {Name: "POSTGRES_DSN", Value: "postgres://vultisig:vultisig@postgres:5432/vultisig-verifier?sslmode=disable"}, {Name: "MINIO_ENDPOINT", Value: "http://minio:9000"}, {Name: "MINIO_ACCESS_KEY", Value: "minioadmin"}, @@ -458,6 +519,28 @@ func testrunnerEnvVars(cfg config.K8sJobConfig) []corev1.EnvVar { {Name: "JWT_SECRET", Value: cfg.JWTSecret}, {Name: "PLUGIN_ENDPOINT", Value: cfg.PluginEndpoint}, } + if cfg.PluginAPIKey != "" { + vars = append(vars, corev1.EnvVar{Name: "PLUGIN_API_KEY", Value: cfg.PluginAPIKey}) + } + if cfg.VaultB64 != "" { + vars = append(vars, corev1.EnvVar{Name: "VAULT_B64", Value: cfg.VaultB64}) + } + if cfg.ServerVaultB64 != "" { + vars = append(vars, corev1.EnvVar{Name: "SERVER_VAULT_B64", Value: cfg.ServerVaultB64}) + } + return vars +} + +func installJobEnvVars(cfg config.K8sJobConfig, pluginID string) []corev1.EnvVar { + vars := testrunnerEnvVars(cfg) + vars = append(vars, + corev1.EnvVar{Name: "RELAY_URL", Value: "https://api.vultisig.com/router"}, + corev1.EnvVar{Name: "PLUGIN_ID", Value: pluginID}, + ) + if cfg.TestTargetAddress != "" { + vars = append(vars, corev1.EnvVar{Name: "TEST_TARGET_ADDRESS", Value: cfg.TestTargetAddress}) + } + return vars } func infraResources() corev1.ResourceRequirements { diff --git a/internal/worker/naming.go b/internal/worker/naming.go index d627567..63cf936 100644 --- a/internal/worker/naming.go +++ b/internal/worker/naming.go @@ -46,6 +46,10 @@ func testJobName(runID string) string { return dnsLabel("test-" + runIDPrefix(runID)) } +func installJobName(runID string) string { + return dnsLabel("install-" + runIDPrefix(runID)) +} + func runLabels(runID, pluginID, kind string) map[string]string { return map[string]string{ labelManagedBy: managedByValue, diff --git a/internal/worker/runner.go b/internal/worker/runner.go index e8b9b50..3ea993a 100644 --- a/internal/worker/runner.go +++ b/internal/worker/runner.go @@ -26,10 +26,12 @@ type Runner struct { } type RunResult struct { - Passed bool - SeederLogs string - TestLogs string - ErrorMsg string + Passed bool + SeederLogs string + TestLogs string + InstallLogs string + VerifierHost string + ErrorMsg string } func NewRunner(k8s kubernetes.Interface, cfg config.K8sJobConfig, logger *logrus.Entry) *Runner { @@ -73,6 +75,14 @@ func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, lab return result } + if r.cfg.IngressDomain != "" { + result.VerifierHost, err = r.deployIngress(ctx, namespace, runID, labels) + if err != nil { + result.ErrorMsg = err.Error() + return result + } + } + err = r.waitForVerifier(ctx, namespace) if err != nil { result.ErrorMsg = err.Error() @@ -92,6 +102,22 @@ func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, lab return result } + if !testPassed { + result.Passed = false + return result + } + + if r.cfg.PluginEndpoint != "" { + var installPassed bool + result.InstallLogs, installPassed, err = r.runInstallJob(ctx, namespace, runID, pluginID, labels) + if err != nil { + result.ErrorMsg = fmt.Sprintf("install job failed: %s", err.Error()) + return result + } + result.Passed = installPassed + return result + } + result.Passed = testPassed return result } @@ -99,7 +125,18 @@ func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, lab func (r *Runner) deployNetworkPolicy(ctx context.Context, ns string) error { r.logger.Info("creating network policy") pluginPort := parsePort(r.cfg.PluginEndpoint) - return createIntraNamespaceNetworkPolicy(ctx, r.k8s, ns, pluginPort) + err := createIntraNamespaceNetworkPolicy(ctx, r.k8s, ns, pluginPort) + if err != nil { + return err + } + if r.cfg.IngressDomain != "" { + err = createIngressNetworkPolicy(ctx, r.k8s, ns) + if err != nil { + return fmt.Errorf("create ingress network policy: %w", err) + } + r.logger.Info("ingress network policy created") + } + return nil } func parsePort(rawURL string) int32 { @@ -231,6 +268,17 @@ func (r *Runner) waitForVerifier(ctx context.Context, ns string) error { return nil } +func (r *Runner) deployIngress(ctx context.Context, ns, runID string, labels map[string]string) (string, error) { + host := fmt.Sprintf("test-%s.%s", runIDPrefix(runID), r.cfg.IngressDomain) + ing := verifierIngress(ns, host, r.cfg.TLSSecretName, labels) + err := applyIngress(ctx, r.k8s, ing) + if err != nil { + return "", fmt.Errorf("deploy ingress: %w", err) + } + r.logger.WithField("host", host).Info("ingress created") + return host, nil +} + func (r *Runner) runSeederJob(ctx context.Context, ns, runID string, labels map[string]string) (string, error) { envVars := testrunnerEnvVars(r.cfg) name := seederJobName(runID) @@ -287,6 +335,32 @@ func (r *Runner) runTestJob(ctx context.Context, ns, runID string, labels map[st return logs, passed, nil } +func (r *Runner) runInstallJob(ctx context.Context, ns, runID, pluginID string, labels map[string]string) (string, bool, error) { + envVars := installJobEnvVars(r.cfg, pluginID) + name := installJobName(runID) + hostAliases := parseHostAliases(r.cfg.HostAliases) + job := installJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) + job.Name = name + + r.logger.WithField("job", name).Info("running install") + _, err := applyJob(ctx, r.k8s, job) + if err != nil { + return "", false, fmt.Errorf("create install job: %w", err) + } + + passed, err := waitForJob(ctx, r.k8s, ns, name, r.timeout(10*time.Minute), r.pollInterval()) + if err != nil { + return "", false, fmt.Errorf("wait for install: %w", err) + } + + logs, err := fetchJobLogsByContainer(ctx, r.k8s, ns, name, "testrunner", 3, 2*time.Second) + if err != nil { + r.logger.WithError(err).Warn("failed to fetch install logs") + } + + return logs, passed, nil +} + func (r *Runner) imageOrDefault(image, defaultImage string) string { if image != "" { return image diff --git a/scripts/fixture-gen/main.go b/scripts/fixture-gen/main.go new file mode 100644 index 0000000..512ac13 --- /dev/null +++ b/scripts/fixture-gen/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "time" + + vaultType "github.com/vultisig/commondata/go/vultisig/vault/v1" + vgcommon "github.com/vultisig/vultisig-go/common" + "google.golang.org/protobuf/proto" +) + +func main() { + vultFile := flag.String("vult", "", "path to .vult file") + password := flag.String("password", "Saggy@Commotion@Occupier@Registry1", "vault encryption password") + flag.Parse() + + if *vultFile == "" { + fmt.Fprintln(os.Stderr, "usage: fixture-gen -vult [-password ]") + os.Exit(1) + } + + raw, err := os.ReadFile(*vultFile) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read file: %v\n", err) + os.Exit(1) + } + + vaultB64 := strings.TrimSpace(string(raw)) + + containerBytes, err := base64.StdEncoding.DecodeString(vaultB64) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to base64-decode .vult content: %v\n", err) + os.Exit(1) + } + + var container vaultType.VaultContainer + err = proto.Unmarshal(containerBytes, &container) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to unmarshal VaultContainer: %v\n", err) + os.Exit(1) + } + + var vaultBytes []byte + if container.IsEncrypted { + if *password == "" { + fmt.Fprintln(os.Stderr, "vault is encrypted — provide -password flag") + os.Exit(1) + } + encBytes, decErr := base64.StdEncoding.DecodeString(container.Vault) + if decErr != nil { + fmt.Fprintf(os.Stderr, "failed to decode encrypted vault string: %v\n", decErr) + os.Exit(1) + } + vaultBytes, err = vgcommon.DecryptVault(*password, encBytes) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to decrypt vault (wrong password?): %v\n", err) + os.Exit(1) + } + } else { + vaultBytes, err = base64.StdEncoding.DecodeString(container.Vault) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to decode vault string: %v\n", err) + os.Exit(1) + } + } + + var vault vaultType.Vault + err = proto.Unmarshal(vaultBytes, &vault) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to unmarshal Vault: %v\n", err) + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "Vault info:\n") + fmt.Fprintf(os.Stderr, " Name: %s\n", vault.Name) + fmt.Fprintf(os.Stderr, " LocalPartyId: %s\n", vault.LocalPartyId) + fmt.Fprintf(os.Stderr, " PublicKeyEcdsa: %s\n", vault.PublicKeyEcdsa) + fmt.Fprintf(os.Stderr, " PublicKeyEddsa: %s\n", vault.PublicKeyEddsa) + fmt.Fprintf(os.Stderr, " Signers: %v\n", vault.Signers) + fmt.Fprintf(os.Stderr, " HexChainCode: %s\n", vault.HexChainCode) + fmt.Fprintf(os.Stderr, " KeyShares: %d\n", len(vault.KeyShares)) + for _, ks := range vault.KeyShares { + fmt.Fprintf(os.Stderr, " - pubkey: %s (keyshare len: %d)\n", ks.PublicKey, len(ks.Keyshare)) + } + + placeholderParties := make([]string, len(vault.Signers)) + for i := range vault.Signers { + placeholderParties[i] = fmt.Sprintf("party%d", i+1) + } + + fixture := map[string]interface{}{ + "vault": map[string]interface{}{ + "public_key": vault.PublicKeyEcdsa, + "name": "integration-test-vault", + "created_at": time.Now().UTC().Format(time.RFC3339), + "vault_b64": vaultB64, + }, + "reshare": map[string]interface{}{ + "session_id": "00000000-0000-0000-0000-000000000000", + "hex_encryption_key": "0000000000000000000000000000000000000000000000000000000000000000", + "hex_chain_code": "0000000000000000000000000000000000000000000000000000000000000000", + "local_party_id": "integration-test-party", + "old_parties": placeholderParties, + "old_reshare_prefix": "integration-test", + "email": "integration@test.example.com", + }, + } + + out, err := json.MarshalIndent(fixture, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal fixture JSON: %v\n", err) + os.Exit(1) + } + + fmt.Println(string(out)) +} From 30141f08589be7b782c4ce815da8921e026be633 Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:16:55 +0400 Subject: [PATCH 5/7] make fixes --- cmd/testrunner/main.go | 14 +++---- internal/api/artifacts.go | 9 +++-- internal/api/results.go | 22 ++++++----- internal/api/templates/detail.html | 15 ++++++-- internal/testrunner/participant.go | 14 +++---- internal/worker/artifacts.go | 12 +++--- internal/worker/manifests.go | 10 ++--- internal/worker/naming.go | 8 ++-- internal/worker/runner.go | 60 +++++++++++++++--------------- 9 files changed, 87 insertions(+), 77 deletions(-) diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go index 5c96b8e..0e6ab85 100644 --- a/cmd/testrunner/main.go +++ b/cmd/testrunner/main.go @@ -16,16 +16,16 @@ func main() { logger.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) if len(os.Args) < 2 { - logger.Fatal("usage: testrunner ") + logger.Fatal("usage: testrunner ") } switch os.Args[1] { case "seed": runSeed() - case "test": - runTest() - case "install": - runInstall() + case "smoke": + runSmoke() + case "integration": + runIntegration() default: logger.Fatalf("unknown command: %s", os.Args[1]) } @@ -70,7 +70,7 @@ func runSeed() { logger.Info("seeding completed successfully") } -func runTest() { +func runSmoke() { fixture, err := testrunner.LoadFixture() if err != nil { logger.WithError(err).Fatal("failed to load fixture") @@ -120,7 +120,7 @@ func runTest() { }).Info("all tests passed") } -func runInstall() { +func runIntegration() { fixture, err := testrunner.LoadFixture() if err != nil { logger.WithError(err).Fatal("failed to load fixture") diff --git a/internal/api/artifacts.go b/internal/api/artifacts.go index 386b135..479b21a 100644 --- a/internal/api/artifacts.go +++ b/internal/api/artifacts.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "io" "net/http" @@ -21,9 +22,9 @@ import ( const maxArtifactReadBytes = 2 << 20 var allowedArtifacts = map[string]bool{ - "seeder.txt": true, - "test.txt": true, - "install.txt": true, + "seeder.txt": true, + "smoke.txt": true, + "integration.txt": true, } func (s *Server) handleGetArtifact(c echo.Context) error { @@ -41,7 +42,7 @@ func (s *Server) handleGetArtifact(c echo.Context) error { pgID := pgtype.UUID{Bytes: parsed, Valid: true} run, err := s.db.Queries().GetTestRun(c.Request().Context(), pgID) if err != nil { - if err == pgx.ErrNoRows { + if errors.Is(err, pgx.ErrNoRows) { return c.JSON(http.StatusNotFound, ErrorResponse{Error: "test run not found"}) } s.logger.WithError(err).Error("failed to get test run") diff --git a/internal/api/results.go b/internal/api/results.go index 09c5700..13daea8 100644 --- a/internal/api/results.go +++ b/internal/api/results.go @@ -2,6 +2,7 @@ package api import ( "embed" + "errors" "fmt" "html/template" "math" @@ -94,9 +95,10 @@ type listPageData struct { } type detailPageData struct { - Run types.TestRun - SeederLogs string - TestLogs string + Run types.TestRun + SeederLogs string + SmokeLogs string + IntegrationLogs string } func (s *Server) handleResultsList(c echo.Context) error { @@ -187,7 +189,7 @@ func (s *Server) handleResultsDetail(c echo.Context) error { run, err := s.db.Queries().GetTestRun(ctx, pgID) if err != nil { - if err == pgx.ErrNoRows { + if errors.Is(err, pgx.ErrNoRows) { return c.String(http.StatusNotFound, "test run not found") } s.logger.WithError(err).Error("failed to get test run") @@ -196,17 +198,19 @@ func (s *Server) handleResultsDetail(c echo.Context) error { result := types.TestRunFromQuery(run) - var seederLogs, testLogs string + var seederLogs, smokeLogs, integrationLogs string if run.ArtifactPrefix.Valid && run.ArtifactPrefix.String != "" && s.artifactS3.Bucket != "" { prefix := run.ArtifactPrefix.String seederLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/seeder.txt") - testLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/test.txt") + smokeLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/smoke.txt") + integrationLogs, _ = readArtifact(ctx, s.artifactS3, prefix+"/integration.txt") } data := detailPageData{ - Run: result, - SeederLogs: seederLogs, - TestLogs: testLogs, + Run: result, + SeederLogs: seederLogs, + SmokeLogs: smokeLogs, + IntegrationLogs: integrationLogs, } return renderHTML(c, detailTmpl, data) diff --git a/internal/api/templates/detail.html b/internal/api/templates/detail.html index faded5d..dfa0089 100644 --- a/internal/api/templates/detail.html +++ b/internal/api/templates/detail.html @@ -58,14 +58,21 @@

Seeder Logs

{{end}} -{{if .TestLogs}} +{{if .SmokeLogs}}
-

Test Logs

-
{{.TestLogs}}
+

Smoke Logs

+
{{.SmokeLogs}}
{{end}} -{{if and (not .SeederLogs) (not .TestLogs)}} +{{if .IntegrationLogs}} +
+

Integration Logs

+
{{.IntegrationLogs}}
+
+{{end}} + +{{if and (not .SeederLogs) (not .SmokeLogs) (not .IntegrationLogs)}}
No artifacts available for this run.
{{end}} {{end}} diff --git a/internal/testrunner/participant.go b/internal/testrunner/participant.go index e50b0bf..255df0a 100644 --- a/internal/testrunner/participant.go +++ b/internal/testrunner/participant.go @@ -13,7 +13,6 @@ import ( "net/http" "slices" "sync" - "sync/atomic" "time" "github.com/google/uuid" @@ -490,9 +489,8 @@ func processQcInbound( wrapper *mpc.Wrapper, logger logrus.FieldLogger, ) (string, string, []byte, error) { - var processedInitiatorMsg atomic.Bool - processedInitiatorMsg.Store(false) - var messageCache sync.Map + processedInitiatorMsg := false + messageCache := make(map[string]bool) relayClient := vgrelay.NewRelayClient(relayURL) start := time.Now() @@ -514,15 +512,15 @@ func processQcInbound( } cacheKey := fmt.Sprintf("%s-%s-%s", sessionID, localPartyID, message.Hash) - if _, found := messageCache.Load(cacheKey); found { + if messageCache[cacheKey] { continue } - if localPartyID != parties[0] && !processedInitiatorMsg.Load() && message.From != parties[0] { + if localPartyID != parties[0] && !processedInitiatorMsg && message.From != parties[0] { logger.Debug("waiting for message from initiator party") continue } - processedInitiatorMsg.Store(true) + processedInitiatorMsg = true inboundBody, decErr := mpc.DecodeDecryptMessage(message.Body, hexEncKey) if decErr != nil { @@ -536,7 +534,7 @@ func processQcInbound( continue } - messageCache.Store(cacheKey, true) + messageCache[cacheKey] = true logger.WithFields(logrus.Fields{ "hash": message.Hash, diff --git a/internal/worker/artifacts.go b/internal/worker/artifacts.go index 486bcef..0009b55 100644 --- a/internal/worker/artifacts.go +++ b/internal/worker/artifacts.go @@ -46,17 +46,17 @@ func (u *ArtifactUploader) UploadRunArtifacts(ctx context.Context, runID string, } } - if result.TestLogs != "" { - err = u.upload(ctx, client, prefix+"/test.txt", result.TestLogs) + if result.SmokeLogs != "" { + err = u.upload(ctx, client, prefix+"/smoke.txt", result.SmokeLogs) if err != nil { - return prefix, fmt.Errorf("failed to upload test logs: %w", err) + return prefix, fmt.Errorf("failed to upload smoke logs: %w", err) } } - if result.InstallLogs != "" { - err = u.upload(ctx, client, prefix+"/install.txt", result.InstallLogs) + if result.IntegrationLogs != "" { + err = u.upload(ctx, client, prefix+"/integration.txt", result.IntegrationLogs) if err != nil { - return prefix, fmt.Errorf("failed to upload install logs: %w", err) + return prefix, fmt.Errorf("failed to upload integration logs: %w", err) } } diff --git a/internal/worker/manifests.go b/internal/worker/manifests.go index 864da25..36d1d9e 100644 --- a/internal/worker/manifests.go +++ b/internal/worker/manifests.go @@ -457,12 +457,12 @@ func seederJob(ns, image, pullSecret string, labels map[string]string, envVars [ return buildTestJob("seeder", ns, image, pullSecret, labels, []string{"seed"}, envVars, ttlSeconds, hostAliases) } -func testJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { - return buildTestJob("test", ns, image, pullSecret, labels, []string{"test"}, envVars, ttlSeconds, hostAliases) +func smokeJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { + return buildTestJob("smoke", ns, image, pullSecret, labels, []string{"smoke"}, envVars, ttlSeconds, hostAliases) } -func installJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { - return buildTestJob("install", ns, image, pullSecret, labels, []string{"install"}, envVars, ttlSeconds, hostAliases) +func integrationJob(ns, image, pullSecret string, labels map[string]string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { + return buildTestJob("integration", ns, image, pullSecret, labels, []string{"integration"}, envVars, ttlSeconds, hostAliases) } func buildTestJob(name, ns, image, pullSecret string, labels map[string]string, args []string, envVars []corev1.EnvVar, ttlSeconds int32, hostAliases []corev1.HostAlias) *batchv1.Job { @@ -531,7 +531,7 @@ func testrunnerEnvVars(cfg config.K8sJobConfig) []corev1.EnvVar { return vars } -func installJobEnvVars(cfg config.K8sJobConfig, pluginID string) []corev1.EnvVar { +func integrationJobEnvVars(cfg config.K8sJobConfig, pluginID string) []corev1.EnvVar { vars := testrunnerEnvVars(cfg) vars = append(vars, corev1.EnvVar{Name: "RELAY_URL", Value: "https://api.vultisig.com/router"}, diff --git a/internal/worker/naming.go b/internal/worker/naming.go index 63cf936..361dc66 100644 --- a/internal/worker/naming.go +++ b/internal/worker/naming.go @@ -42,12 +42,12 @@ func seederJobName(runID string) string { return dnsLabel("seeder-" + runIDPrefix(runID)) } -func testJobName(runID string) string { - return dnsLabel("test-" + runIDPrefix(runID)) +func smokeJobName(runID string) string { + return dnsLabel("smoke-" + runIDPrefix(runID)) } -func installJobName(runID string) string { - return dnsLabel("install-" + runIDPrefix(runID)) +func integrationJobName(runID string) string { + return dnsLabel("integration-" + runIDPrefix(runID)) } func runLabels(runID, pluginID, kind string) map[string]string { diff --git a/internal/worker/runner.go b/internal/worker/runner.go index 3ea993a..5230be7 100644 --- a/internal/worker/runner.go +++ b/internal/worker/runner.go @@ -26,12 +26,12 @@ type Runner struct { } type RunResult struct { - Passed bool - SeederLogs string - TestLogs string - InstallLogs string - VerifierHost string - ErrorMsg string + Passed bool + SeederLogs string + SmokeLogs string + IntegrationLogs string + VerifierHost string + ErrorMsg string } func NewRunner(k8s kubernetes.Interface, cfg config.K8sJobConfig, logger *logrus.Entry) *Runner { @@ -95,30 +95,30 @@ func (r *Runner) Run(ctx context.Context, namespace, runID, pluginID string, lab return result } - testLogs, testPassed, err := r.runTestJob(ctx, namespace, runID, labels) - result.TestLogs = testLogs + smokeLogs, smokePassed, err := r.runSmokeJob(ctx, namespace, runID, labels) + result.SmokeLogs = smokeLogs if err != nil { - result.ErrorMsg = fmt.Sprintf("test job failed: %s", err.Error()) + result.ErrorMsg = fmt.Sprintf("smoke job failed: %s", err.Error()) return result } - if !testPassed { + if !smokePassed { result.Passed = false return result } if r.cfg.PluginEndpoint != "" { - var installPassed bool - result.InstallLogs, installPassed, err = r.runInstallJob(ctx, namespace, runID, pluginID, labels) + var integrationPassed bool + result.IntegrationLogs, integrationPassed, err = r.runIntegrationJob(ctx, namespace, runID, pluginID, labels) if err != nil { - result.ErrorMsg = fmt.Sprintf("install job failed: %s", err.Error()) + result.ErrorMsg = fmt.Sprintf("integration job failed: %s", err.Error()) return result } - result.Passed = installPassed + result.Passed = integrationPassed return result } - result.Passed = testPassed + result.Passed = smokePassed return result } @@ -309,53 +309,53 @@ func (r *Runner) runSeederJob(ctx context.Context, ns, runID string, labels map[ return logs, nil } -func (r *Runner) runTestJob(ctx context.Context, ns, runID string, labels map[string]string) (string, bool, error) { +func (r *Runner) runSmokeJob(ctx context.Context, ns, runID string, labels map[string]string) (string, bool, error) { envVars := testrunnerEnvVars(r.cfg) - name := testJobName(runID) + name := smokeJobName(runID) hostAliases := parseHostAliases(r.cfg.HostAliases) - job := testJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) + job := smokeJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) job.Name = name - r.logger.WithField("job", name).Info("running tests") + r.logger.WithField("job", name).Info("running smoke tests") _, err := applyJob(ctx, r.k8s, job) if err != nil { - return "", false, fmt.Errorf("create test job: %w", err) + return "", false, fmt.Errorf("create smoke job: %w", err) } passed, err := waitForJob(ctx, r.k8s, ns, name, r.timeout(10*time.Minute), r.pollInterval()) if err != nil { - return "", false, fmt.Errorf("wait for test: %w", err) + return "", false, fmt.Errorf("wait for smoke: %w", err) } logs, logErr := fetchJobLogsByContainer(ctx, r.k8s, ns, name, "testrunner", 3, 2*time.Second) if logErr != nil { - r.logger.WithError(logErr).Warn("failed to fetch test logs") + r.logger.WithError(logErr).Warn("failed to fetch smoke logs") } return logs, passed, nil } -func (r *Runner) runInstallJob(ctx context.Context, ns, runID, pluginID string, labels map[string]string) (string, bool, error) { - envVars := installJobEnvVars(r.cfg, pluginID) - name := installJobName(runID) +func (r *Runner) runIntegrationJob(ctx context.Context, ns, runID, pluginID string, labels map[string]string) (string, bool, error) { + envVars := integrationJobEnvVars(r.cfg, pluginID) + name := integrationJobName(runID) hostAliases := parseHostAliases(r.cfg.HostAliases) - job := installJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) + job := integrationJob(ns, r.cfg.TestImage, r.cfg.ImagePullSecret, labels, envVars, r.cfg.TTLAfterFinished, hostAliases) job.Name = name - r.logger.WithField("job", name).Info("running install") + r.logger.WithField("job", name).Info("running integration") _, err := applyJob(ctx, r.k8s, job) if err != nil { - return "", false, fmt.Errorf("create install job: %w", err) + return "", false, fmt.Errorf("create integration job: %w", err) } passed, err := waitForJob(ctx, r.k8s, ns, name, r.timeout(10*time.Minute), r.pollInterval()) if err != nil { - return "", false, fmt.Errorf("wait for install: %w", err) + return "", false, fmt.Errorf("wait for integration: %w", err) } logs, err := fetchJobLogsByContainer(ctx, r.k8s, ns, name, "testrunner", 3, 2*time.Second) if err != nil { - r.logger.WithError(err).Warn("failed to fetch install logs") + r.logger.WithError(err).Warn("failed to fetch integration logs") } return logs, passed, nil From 3b2397f4471fcb8de131ab7c25b1e8ee356d6f63 Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:10:37 +0400 Subject: [PATCH 6/7] apply fixes based on review --- internal/api/handler.go | 21 ++++++++++++++++++--- internal/testrunner/jwt.go | 3 +++ internal/testrunner/schema/schema.sql | 2 +- internal/testrunner/tests.go | 12 ++++++++++++ internal/worker/manifests_test.go | 8 ++++---- internal/worker/naming_test.go | 6 +++--- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/internal/api/handler.go b/internal/api/handler.go index c59734a..90bfdd3 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -2,6 +2,7 @@ package api import ( "errors" + "fmt" "net/http" "strconv" "strings" @@ -139,7 +140,10 @@ func (s *Server) handleListTestRuns(c echo.Context) error { offset = parsed } - filterParams := buildFilterParams(c) + filterParams, err := buildFilterParams(c) + if err != nil { + return c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) + } ctx := c.Request().Context() @@ -181,16 +185,27 @@ type filterParams struct { Status queries.NullTestRunStatus } -func buildFilterParams(c echo.Context) filterParams { +var validStatuses = map[string]bool{ + "QUEUED": true, + "RUNNING": true, + "PASSED": true, + "FAILED": true, + "ERROR": true, +} + +func buildFilterParams(c echo.Context) (filterParams, error) { var fp filterParams if v := strings.TrimSpace(c.QueryParam("plugin_id")); v != "" { fp.PluginID = pgtype.Text{String: v, Valid: true} } if v := strings.TrimSpace(c.QueryParam("status")); v != "" { + if !validStatuses[v] { + return fp, fmt.Errorf("invalid status: %s", v) + } fp.Status = queries.NullTestRunStatus{ TestRunStatus: queries.TestRunStatus(v), Valid: true, } } - return fp + return fp, nil } diff --git a/internal/testrunner/jwt.go b/internal/testrunner/jwt.go index 68ae988..14cebec 100644 --- a/internal/testrunner/jwt.go +++ b/internal/testrunner/jwt.go @@ -18,6 +18,9 @@ func GenerateJWT(secret, pubkey, tokenID string, expireHours int) (string, error if secret == "" || pubkey == "" { return "", fmt.Errorf("secret and pubkey are required") } + if expireHours <= 0 { + return "", fmt.Errorf("expireHours must be > 0") + } expirationTime := time.Now().Add(time.Duration(expireHours) * time.Hour) claims := &Claims{ diff --git a/internal/testrunner/schema/schema.sql b/internal/testrunner/schema/schema.sql index 63ec2f5..277e68e 100644 --- a/internal/testrunner/schema/schema.sql +++ b/internal/testrunner/schema/schema.sql @@ -36,7 +36,7 @@ CREATE TABLE vault_tokens ( CREATE TABLE plugin_policies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), public_key TEXT NOT NULL, - plugin_id plugin_id NOT NULL, + plugin_id plugin_id NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, plugin_version TEXT NOT NULL, policy_version INTEGER NOT NULL, signature TEXT NOT NULL, diff --git a/internal/testrunner/tests.go b/internal/testrunner/tests.go index 3cb8014..93b6f3c 100644 --- a/internal/testrunner/tests.go +++ b/internal/testrunner/tests.go @@ -111,6 +111,9 @@ func (s *TestSuite) healthChecks() bool { return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("expected 2xx, got %d", resp.StatusCode) + } return nil }) @@ -241,6 +244,9 @@ func (s *TestSuite) pluginEndpointTests() bool { return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("expected 2xx, got %d", resp.StatusCode) + } return nil }) @@ -250,6 +256,9 @@ func (s *TestSuite) pluginEndpointTests() bool { return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("expected 2xx, got %d", resp.StatusCode) + } return nil }) @@ -259,6 +268,9 @@ func (s *TestSuite) pluginEndpointTests() bool { return fmt.Errorf("plugin unreachable: %w", err) } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("expected 2xx, got %d", resp.StatusCode) + } return nil }) } diff --git a/internal/worker/manifests_test.go b/internal/worker/manifests_test.go index 3b9c6ae..ecbf7a6 100644 --- a/internal/worker/manifests_test.go +++ b/internal/worker/manifests_test.go @@ -158,13 +158,13 @@ func TestSeederJob(t *testing.T) { assert.Equal(t, int32(300), *job.Spec.TTLSecondsAfterFinished) } -func TestTestJob(t *testing.T) { +func TestSmokeJob(t *testing.T) { envVars := testrunnerEnvVars(testK8sCfg) - job := testJob("ns", "testrunner:v1", "my-secret", nil, envVars, 0, nil) + job := smokeJob("ns", "testrunner:v1", "my-secret", nil, envVars, 0, nil) - assert.Equal(t, "test", job.Name) + assert.Equal(t, "smoke", job.Name) require.Len(t, job.Spec.Template.Spec.Containers, 1) - assert.Equal(t, []string{"test"}, job.Spec.Template.Spec.Containers[0].Args) + assert.Equal(t, []string{"smoke"}, job.Spec.Template.Spec.Containers[0].Args) assert.Nil(t, job.Spec.TTLSecondsAfterFinished) require.Len(t, job.Spec.Template.Spec.ImagePullSecrets, 1) } diff --git a/internal/worker/naming_test.go b/internal/worker/naming_test.go index e9f1d97..d1a9e4c 100644 --- a/internal/worker/naming_test.go +++ b/internal/worker/naming_test.go @@ -122,9 +122,9 @@ func TestSeederJobName(t *testing.T) { assert.LessOrEqual(t, len(result), maxDNSLabelLen) } -func TestTestJobName(t *testing.T) { - result := testJobName("550e8400-e29b-41d4-a716-446655440000") - assert.Equal(t, "test-550e8400e29b", result) +func TestSmokeJobName(t *testing.T) { + result := smokeJobName("550e8400-e29b-41d4-a716-446655440000") + assert.Equal(t, "smoke-550e8400e29b", result) assert.LessOrEqual(t, len(result), maxDNSLabelLen) } From d40a3a613036b5b41683625a9b9f4857439d1c74 Mon Sep 17 00:00:00 2001 From: 4others <44651681+4others@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:04:08 +0400 Subject: [PATCH 7/7] iterate on fixes --- internal/api/handler.go | 3 ++- internal/api/results.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/api/handler.go b/internal/api/handler.go index 90bfdd3..4a10527 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "math" "net/http" "strconv" "strings" @@ -134,7 +135,7 @@ func (s *Server) handleListTestRuns(c echo.Context) error { } if v := c.QueryParam("offset"); v != "" { parsed, err := strconv.Atoi(v) - if err != nil || parsed < 0 { + if err != nil || parsed < 0 || parsed > math.MaxInt32 { return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "invalid offset parameter"}) } offset = parsed diff --git a/internal/api/results.go b/internal/api/results.go index 13daea8..9c6f0a8 100644 --- a/internal/api/results.go +++ b/internal/api/results.go @@ -108,6 +108,9 @@ func (s *Server) handleResultsList(c echo.Context) error { if page < 1 { page = 1 } + if page > math.MaxInt32/perPage { + page = math.MaxInt32 / perPage + } } offset := (page - 1) * perPage