From 66242eab41e68dddf70df708c60e4ae4d79e6fff Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Fri, 17 Jul 2020 13:25:28 -0700 Subject: [PATCH] Long-term ledger storage with BigTable (bp #11222) --- Cargo.toml | 1 + ci/run-sanity.sh | 2 +- core/Cargo.toml | 11 +- core/src/rpc.rs | 117 +- core/src/rpc_service.rs | 35 +- core/src/rpc_subscriptions.rs | 6 +- core/tests/rpc.rs | 8 +- .../rpc-transaction-history.md | 106 ++ ledger-tool/Cargo.toml | 7 +- ledger-tool/src/bigtable.rs | 548 ++++++++ ledger-tool/src/main.rs | 6 +- ledger/src/blockstore.rs | 2 +- multinode-demo/bootstrap-validator.sh | 3 + storage-bigtable/Cargo.toml | 36 + storage-bigtable/README.md | 22 + storage-bigtable/build-proto/.gitignore | 2 + storage-bigtable/build-proto/Cargo.lock | 340 +++++ storage-bigtable/build-proto/Cargo.toml | 15 + storage-bigtable/build-proto/README.md | 2 + storage-bigtable/build-proto/build.sh | 10 + storage-bigtable/build-proto/src/main.rs | 19 + storage-bigtable/init-bigtable.sh | 27 + storage-bigtable/proto/google.api.rs | 632 +++++++++ storage-bigtable/proto/google.bigtable.v2.rs | 1057 ++++++++++++++ storage-bigtable/proto/google.protobuf.rs | 1 + storage-bigtable/proto/google.rpc.rs | 22 + storage-bigtable/src/access_token.rs | 118 ++ storage-bigtable/src/bigtable.rs | 466 +++++++ storage-bigtable/src/compression.rs | 105 ++ storage-bigtable/src/lib.rs | 536 ++++++++ storage-bigtable/src/pki-goog-roots.pem | 1222 +++++++++++++++++ storage-bigtable/src/root_ca_certificate.rs | 20 + transaction-status/src/lib.rs | 2 +- validator/src/main.rs | 12 +- 34 files changed, 5484 insertions(+), 34 deletions(-) create mode 100644 docs/src/implemented-proposals/rpc-transaction-history.md create mode 100644 ledger-tool/src/bigtable.rs create mode 100644 storage-bigtable/Cargo.toml create mode 100644 storage-bigtable/README.md create mode 100644 storage-bigtable/build-proto/.gitignore create mode 100644 storage-bigtable/build-proto/Cargo.lock create mode 100644 storage-bigtable/build-proto/Cargo.toml create mode 100644 storage-bigtable/build-proto/README.md create mode 100755 storage-bigtable/build-proto/build.sh create mode 100644 storage-bigtable/build-proto/src/main.rs create mode 100755 storage-bigtable/init-bigtable.sh create mode 100644 storage-bigtable/proto/google.api.rs create mode 100644 storage-bigtable/proto/google.bigtable.v2.rs create mode 100644 storage-bigtable/proto/google.protobuf.rs create mode 100644 storage-bigtable/proto/google.rpc.rs create mode 100644 storage-bigtable/src/access_token.rs create mode 100644 storage-bigtable/src/bigtable.rs create mode 100644 storage-bigtable/src/compression.rs create mode 100644 storage-bigtable/src/lib.rs create mode 100644 storage-bigtable/src/pki-goog-roots.pem create mode 100644 storage-bigtable/src/root_ca_certificate.rs diff --git a/Cargo.toml b/Cargo.toml index 461fa0db71..d37b9f78ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "log-analyzer", "merkle-tree", "stake-o-matic", + "storage-bigtable", "streamer", "measure", "metrics", diff --git a/ci/run-sanity.sh b/ci/run-sanity.sh index 54cf44a501..36f423c53e 100755 --- a/ci/run-sanity.sh +++ b/ci/run-sanity.sh @@ -7,7 +7,7 @@ source multinode-demo/common.sh rm -rf config/run/init-completed config/ledger config/snapshot-ledger -timeout 15 ./run.sh & +timeout 120 ./run.sh & pid=$! attempts=20 diff --git a/core/Cargo.toml b/core/Cargo.toml index fbab82c0d8..61bb7f81bb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -59,20 +59,21 @@ solana-perf = { path = "../perf", version = "1.2.20" } solana-runtime = { path = "../runtime", version = "1.2.20" } solana-sdk = { path = "../sdk", version = "1.2.20" } solana-stake-program = { path = "../programs/stake", version = "1.2.20" } +solana-storage-bigtable = { path = "../storage-bigtable", version = "1.2.20" } solana-streamer = { path = "../streamer", version = "1.2.20" } solana-sys-tuner = { path = "../sys-tuner", version = "1.2.20" } solana-transaction-status = { path = "../transaction-status", version = "1.2.20" } solana-version = { path = "../version", version = "1.2.20" } solana-vote-program = { path = "../programs/vote", version = "1.2.20" } solana-vote-signer = { path = "../vote-signer", version = "1.2.20" } +solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "1.2.20" } spl-token-v1-0 = { package = "spl-token", version = "1.0.6", features = ["skip-no-mangle"] } tempfile = "3.1.0" thiserror = "1.0" -tokio = "0.1" -tokio-codec = "0.1" -tokio-fs = "0.1" -tokio-io = "0.1" -solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "1.2.20" } +tokio = { version = "0.2.22", features = ["full"] } +tokio_01 = { version = "0.1", package = "tokio" } +tokio_fs_01 = { version = "0.1", package = "tokio-fs" } +tokio_io_01 = { version = "0.1", package = "tokio-io" } trees = "0.2.1" [dev-dependencies] diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 6703db0722..23bd2fbf57 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -64,6 +64,7 @@ use std::{ str::FromStr, sync::{Arc, RwLock}, }; +use tokio::runtime; fn new_response(bank: &Bank, value: T) -> Result> { let context = RpcResponseContext { slot: bank.slot() }; @@ -78,6 +79,7 @@ pub struct JsonRpcConfig { pub identity_pubkey: Pubkey, pub faucet_addr: Option, pub health_check_slot_distance: u64, + pub enable_bigtable_ledger_storage: bool, } #[derive(Clone)] @@ -91,6 +93,8 @@ pub struct JsonRpcRequestProcessor { cluster_info: Arc, genesis_hash: Hash, send_transaction_service: Arc, + runtime_handle: runtime::Handle, + bigtable_ledger_storage: Option, } impl Metadata for JsonRpcRequestProcessor {} @@ -145,6 +149,7 @@ impl JsonRpcRequestProcessor { } } + #[allow(clippy::too_many_arguments)] pub fn new( config: JsonRpcConfig, bank_forks: Arc>, @@ -155,6 +160,8 @@ impl JsonRpcRequestProcessor { cluster_info: Arc, genesis_hash: Hash, send_transaction_service: Arc, + runtime: &runtime::Runtime, + bigtable_ledger_storage: Option, ) -> Self { Self { config, @@ -166,6 +173,8 @@ impl JsonRpcRequestProcessor { cluster_info, genesis_hash, send_transaction_service, + runtime_handle: runtime.handle().clone(), + bigtable_ledger_storage, } } @@ -507,6 +516,7 @@ impl JsonRpcRequestProcessor { slot: Slot, encoding: Option, ) -> Result> { + let encoding = encoding.unwrap_or(UiTransactionEncoding::Json); if self.config.enable_rpc_transaction_history && slot <= self @@ -515,7 +525,15 @@ impl JsonRpcRequestProcessor { .unwrap() .largest_confirmed_root() { - let result = self.blockstore.get_confirmed_block(slot, encoding); + let result = self.blockstore.get_confirmed_block(slot, Some(encoding)); + if result.is_err() { + if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { + return Ok(self + .runtime_handle + .block_on(bigtable_ledger_storage.get_confirmed_block(slot, encoding)) + .ok()); + } + } self.check_slot_cleaned_up(&result, slot)?; Ok(result.ok()) } else { @@ -544,9 +562,25 @@ impl JsonRpcRequestProcessor { MAX_GET_CONFIRMED_BLOCKS_RANGE ))); } + + let lowest_slot = self.blockstore.lowest_slot(); + if start_slot < lowest_slot { + // If the starting slot is lower than what's available in blockstore assume the entire + // [start_slot..end_slot] can be fetched from BigTable. + if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { + return Ok(self + .runtime_handle + .block_on( + bigtable_ledger_storage + .get_confirmed_blocks(start_slot, (end_slot - start_slot) as usize), + ) + .unwrap_or_else(|_| vec![])); + } + } + Ok(self .blockstore - .rooted_slot_iterator(max(start_slot, self.blockstore.lowest_slot())) + .rooted_slot_iterator(max(start_slot, lowest_slot)) .map_err(|_| Error::internal_error())? .filter(|&slot| slot <= end_slot) .collect()) @@ -640,6 +674,16 @@ impl JsonRpcRequestProcessor { err, } }) + .or_else(|| { + if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { + self.runtime_handle + .block_on(bigtable_ledger_storage.get_signature_status(&signature)) + .map(Some) + .unwrap_or(None) + } else { + None + } + }) } else { None }; @@ -681,23 +725,39 @@ impl JsonRpcRequestProcessor { &self, signature: Signature, encoding: Option, - ) -> Result> { + ) -> Option { + let encoding = encoding.unwrap_or(UiTransactionEncoding::Json); if self.config.enable_rpc_transaction_history { - Ok(self + match self .blockstore - .get_confirmed_transaction(signature, encoding) + .get_confirmed_transaction(signature, Some(encoding)) .unwrap_or(None) - .filter(|confirmed_transaction| { - confirmed_transaction.slot + { + Some(confirmed_transaction) => { + if confirmed_transaction.slot <= self .block_commitment_cache .read() .unwrap() - .largest_confirmed_root() - })) - } else { - Ok(None) + .highest_confirmed_slot() + { + return Some(confirmed_transaction); + } + } + None => { + if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { + return self + .runtime_handle + .block_on( + bigtable_ledger_storage + .get_confirmed_transaction(&signature, encoding), + ) + .unwrap_or(None); + } + } + } } + None } pub fn get_confirmed_signatures_for_address( @@ -707,6 +767,8 @@ impl JsonRpcRequestProcessor { end_slot: Slot, ) -> Result> { if self.config.enable_rpc_transaction_history { + // TODO: Add bigtable_ledger_storage support as a part of + // https://github.com/solana-labs/solana/pull/10928 let end_slot = min( end_slot, self.block_commitment_cache @@ -724,10 +786,23 @@ impl JsonRpcRequestProcessor { } pub fn get_first_available_block(&self) -> Result { - Ok(self + let slot = self .blockstore .get_first_available_block() - .unwrap_or_default()) + .unwrap_or_default(); + + if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { + let bigtable_slot = self + .runtime_handle + .block_on(bigtable_ledger_storage.get_first_available_block()) + .unwrap_or(None) + .unwrap_or(slot); + + if bigtable_slot < slot { + return Ok(bigtable_slot); + } + } + Ok(slot) } pub fn get_confirmed_signatures_for_address2( @@ -2008,7 +2083,7 @@ impl RpcSol for RpcSolImpl { signature_str ); let signature = verify_signature(&signature_str)?; - meta.get_confirmed_transaction(signature, encoding) + Ok(meta.get_confirmed_transaction(signature, encoding)) } fn get_confirmed_signatures_for_address( @@ -2379,6 +2454,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); cluster_info.insert_info(ContactInfo::new_with_pubkey_socketaddr( @@ -2430,6 +2507,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); thread::spawn(move || { let blockhash = bank.confirmed_last_blockhash().0; @@ -3526,6 +3605,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); let req = r#"{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["37u9WtQpcm6ULa3Vmu7ySnANv"]}"#; @@ -3570,6 +3651,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); let mut bad_transaction = @@ -3764,6 +3847,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); assert_eq!(request_processor.validator_exit(), Ok(false)); assert_eq!(exit.load(Ordering::Relaxed), false); @@ -3796,6 +3881,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); assert_eq!(request_processor.validator_exit(), Ok(true)); assert_eq!(exit.load(Ordering::Relaxed), true); @@ -3887,6 +3974,8 @@ pub mod tests { &bank_forks, &exit, )), + &runtime::Runtime::new().unwrap(), + None, ); assert_eq!( request_processor.get_block_commitment(0), diff --git a/core/src/rpc_service.rs b/core/src/rpc_service.rs index e1065a6b0c..cf3ffdb554 100644 --- a/core/src/rpc_service.rs +++ b/core/src/rpc_service.rs @@ -24,7 +24,7 @@ use std::{ sync::{mpsc::channel, Arc, RwLock}, thread::{self, Builder, JoinHandle}, }; -use tokio::prelude::Future; +use tokio::runtime; pub struct JsonRpcService { thread_hdl: JoinHandle<()>, @@ -33,6 +33,7 @@ pub struct JsonRpcService { pub request_processor: JsonRpcRequestProcessor, // Used only by test_rpc_new()... close_handle: Option, + runtime: runtime::Runtime, } struct RpcRequestMiddleware { @@ -98,6 +99,9 @@ impl RpcRequestMiddleware { } fn process_file_get(&self, path: &str) -> RequestMiddlewareAction { + // Stuck on tokio 0.1 until the jsonrpc-http-server crate upgrades to tokio 0.2 + use tokio_01::prelude::*; + let stem = path.split_at(1).1; // Drop leading '/' from path let filename = { match path { @@ -116,10 +120,10 @@ impl RpcRequestMiddleware { RequestMiddlewareAction::Respond { should_validate_hosts: true, response: Box::new( - tokio_fs::file::File::open(filename) + tokio_fs_01::file::File::open(filename) .and_then(|file| { let buf: Vec = Vec::new(); - tokio_io::io::read_to_end(file, buf) + tokio_io_01::io::read_to_end(file, buf) .and_then(|item| Ok(hyper::Response::new(item.1.into()))) .or_else(|_| Ok(RpcRequestMiddleware::internal_server_error())) }) @@ -256,6 +260,27 @@ impl JsonRpcService { &exit_send_transaction_service, )); + let mut runtime = runtime::Builder::new() + .threaded_scheduler() + .thread_name("rpc-runtime") + .enable_all() + .build() + .expect("Runtime"); + + let bigtable_ledger_storage = if config.enable_bigtable_ledger_storage { + runtime + .block_on(solana_storage_bigtable::LedgerStorage::new(false)) + .map(|x| { + info!("BigTable ledger storage initialized"); + Some(x) + }) + .unwrap_or_else(|err| { + error!("Failed to initialize BigTable ledger storage: {:?}", err); + None + }) + } else { + None + }; let request_processor = JsonRpcRequestProcessor::new( config, bank_forks.clone(), @@ -266,6 +291,8 @@ impl JsonRpcService { cluster_info, genesis_hash, send_transaction_service, + &runtime, + bigtable_ledger_storage, ); #[cfg(test)] @@ -325,6 +352,7 @@ impl JsonRpcService { .register_exit(Box::new(move || close_handle_.close())); Self { thread_hdl, + runtime, #[cfg(test)] request_processor: test_request_processor, close_handle: Some(close_handle), @@ -338,6 +366,7 @@ impl JsonRpcService { } pub fn join(self) -> thread::Result<()> { + self.runtime.shutdown_background(); self.thread_hdl.join() } } diff --git a/core/src/rpc_subscriptions.rs b/core/src/rpc_subscriptions.rs index 9b63b24755..e93118f26a 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -36,7 +36,9 @@ use std::{ iter, sync::{Arc, Mutex, RwLock}, }; -use tokio::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor}; + +// Stuck on tokio 0.1 until the jsonrpc-pubsub crate upgrades to tokio 0.2 +use tokio_01::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor}; const RECEIVE_DELAY_MILLIS: u64 = 100; @@ -965,7 +967,7 @@ pub(crate) mod tests { system_transaction, }; use std::{fmt::Debug, sync::mpsc::channel, time::Instant}; - use tokio::{prelude::FutureExt, runtime::Runtime, timer::Delay}; + use tokio_01::{prelude::FutureExt, runtime::Runtime, timer::Delay}; pub(crate) fn robust_poll_or_panic( receiver: futures::sync::mpsc::Receiver, diff --git a/core/tests/rpc.rs b/core/tests/rpc.rs index 37bb955e42..9d11f1e16b 100644 --- a/core/tests/rpc.rs +++ b/core/tests/rpc.rs @@ -26,7 +26,7 @@ use std::{ thread::sleep, time::{Duration, Instant}, }; -use tokio::runtime::Runtime; +use tokio_01::runtime::Runtime; macro_rules! json_req { ($method: expr, $params: expr) => {{ @@ -189,7 +189,7 @@ fn test_rpc_subscriptions() { .and_then(move |client| { for sig in signature_set { let status_sender = status_sender.clone(); - tokio::spawn( + tokio_01::spawn( client .signature_subscribe(sig.clone(), None) .and_then(move |sig_stream| { @@ -203,7 +203,7 @@ fn test_rpc_subscriptions() { }), ); } - tokio::spawn( + tokio_01::spawn( client .slot_subscribe() .and_then(move |slot_stream| { @@ -218,7 +218,7 @@ fn test_rpc_subscriptions() { ); for pubkey in account_set { let account_sender = account_sender.clone(); - tokio::spawn( + tokio_01::spawn( client .account_subscribe(pubkey, None) .and_then(move |account_stream| { diff --git a/docs/src/implemented-proposals/rpc-transaction-history.md b/docs/src/implemented-proposals/rpc-transaction-history.md new file mode 100644 index 0000000000..17fb563968 --- /dev/null +++ b/docs/src/implemented-proposals/rpc-transaction-history.md @@ -0,0 +1,106 @@ +# Long term RPC Transaction History +There's a need for RPC to serve at least 6 months of transaction history. The +current history, on the order of days, is insufficient for downstream users. + +6 months of transaction data cannot be stored practically in a validator's +rocksdb ledger so an external data store is necessary. The validator's +rocksdb ledger will continue to serve as the primary data source, and then will +fall back to the external data store. + +The affected RPC endpoints are: +* [getFirstAvailableBlock](https://docs.solana.com/apps/jsonrpc-api#getfirstavailableblock) +* [getConfirmedBlock](https://docs.solana.com/apps/jsonrpc-api#getconfirmedblock) +* [getConfirmedBlocks](https://docs.solana.com/apps/jsonrpc-api#getconfirmedblocks) +* [getConfirmedSignaturesForAddress](https://docs.solana.com/apps/jsonrpc-api#getconfirmedsignaturesforaddress) +* [getConfirmedTransaction](https://docs.solana.com/apps/jsonrpc-api#getconfirmedtransaction) +* [getSignatureStatuses](https://docs.solana.com/apps/jsonrpc-api#getsignaturestatuses) + +Note that [getBlockTime](https://docs.solana.com/apps/jsonrpc-api#getblocktime) +is not supported, as once https://github.com/solana-labs/solana/issues/10089 is +fixed then `getBlockTime` can be removed. + +Some system design constraints: +* The volume of data to store and search can quickly jump into the terabytes, + and is immutable. +* The system should be as light as possible for SREs. For example an SQL + database cluster that requires an SRE to continually monitor and rebalance + nodes is undesirable. +* Data must be searchable in real time - batched queries that take minutes or + hours to run are unacceptable. +* Easy to replicate the data worldwide to co-locate it with the RPC endpoints + that will utilize it. +* Interfacing with the external data store should be easy and not require + depending on risky lightly-used community-supported code libraries + +Based on these constraints, Google's BigTable product is selected as the data +store. + +## Table Schema +A BigTable instance is used to hold all transaction data, broken up into +different tables for quick searching. + +New data may be copied into the instance at anytime without affecting the existing +data, and all data is immutable. Generally the expectation is that new data +will be uploaded once an current epoch completes but there is no limitation on +the frequency of data dumps. + +Cleanup of old data is automatic by configuring the data retention policy of the +instance tables appropriately, it just disappears. Therefore the order of when data is +added becomes important. For example if data from epoch N-1 is added after data +from epoch N, the older epoch data will outlive the newer data. However beyond +producing _holes_ in query results, this kind of unordered deletion will +have no ill effect. Note that this method of cleanup effectively allows for an +unlimited amount of transaction data to be stored, restricted only by the +monetary costs of doing so. + +The table layout s supports the existing RPC endpoints only. New RPC endpoints +in the future may require additions to the schema and potentially iterating over +all transactions to build up the necessary metadata. + +## Accessing BigTable +BigTable has a gRPC endpoint that can be accessed using the +[tonic](https://crates.io/crates/crate)] and the raw protobuf API, as currently no +higher-level Rust crate for BigTable exists. Practically this makes parsing the +results of BigTable queries more complicated but is not a significant issue. + +## Data Population +The ongoing population of instance data will occur on an epoch cadence through the +use of a new `solana-ledger-tool` command that will convert rocksdb data for a +given slot range into the instance schema. + +The same process will be run once, manually, to backfill the existing ledger +data. + +### Block Table: `block` + +This table contains the compressed block data for a given slot. + +The row key is generated by taking the 16 digit lower case hexadecimal +representation of the slot, to ensure that the oldest slot with a confirmed +block will always be first when the rows are listed. eg, The row key for slot +42 would be 000000000000002a. + +The row data is a compressed `StoredConfirmedBlock` struct. + + +### Account Address Transaction Signature Lookup Table: `tx-by-addr` + +This table contains the transactions that affect a given address. + +The row key is `/`. The row +data is a compressed `TransactionByAddrInfo` struct. + +Taking the one's compliment of the slot allows for listing of slots ensures that +the newest slot with transactions that affect an address will always +be listed first. + +Sysvar addresses are not indexed. However frequently used programs such as +Vote or System are, and will likely have a row for every confirmed slot. + +### Transaction Signature Lookup Table: `tx` + +This table maps a transaction signature to its confirmed block, and index within that block. + +The row key is the base58-encoded transaction signature. +The row data is a compressed `TransactionInfo` struct. diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index 5ec6e0603b..49c019cc31 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -12,22 +12,27 @@ homepage = "https://solana.com/" bs58 = "0.3.1" bytecount = "0.6.0" clap = "2.33.1" +futures = "0.3.5" +futures-util = "0.3.5" histogram = "*" log = { version = "0.4.8" } +regex = "1" serde_json = "1.0.53" serde_yaml = "0.8.12" solana-clap-utils = { path = "../clap-utils", version = "1.2.20" } solana-cli = { path = "../cli", version = "1.2.20" } solana-ledger = { path = "../ledger", version = "1.2.20" } solana-logger = { path = "../logger", version = "1.2.20" } +solana-measure = { path = "../measure", version = "1.2.20" } solana-runtime = { path = "../runtime", version = "1.2.20" } solana-sdk = { path = "../sdk", version = "1.2.20" } solana-stake-program = { path = "../programs/stake", version = "1.2.20" } +solana-storage-bigtable = { path = "../storage-bigtable", version = "1.2.20" } solana-transaction-status = { path = "../transaction-status", version = "1.2.20" } solana-version = { path = "../version", version = "1.2.20" } solana-vote-program = { path = "../programs/vote", version = "1.2.20" } tempfile = "3.1.0" -regex = "1" +tokio = { version = "0.2.22", features = ["full"] } [dev-dependencies] assert_cmd = "1.0" diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs new file mode 100644 index 0000000000..61520ca675 --- /dev/null +++ b/ledger-tool/src/bigtable.rs @@ -0,0 +1,548 @@ +/// The `bigtable` subcommand +use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; +use log::*; +use solana_clap_utils::{ + input_parsers::pubkey_of, + input_validators::{is_slot, is_valid_pubkey}, +}; +use solana_cli::display::println_transaction; +use solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}; +use solana_measure::measure::Measure; +use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}; +use solana_transaction_status::UiTransactionEncoding; +use std::{collections::HashSet, path::Path, process::exit, result::Result, time::Duration}; +use tokio::time::delay_for; + +// Attempt to upload this many blocks in parallel +const NUM_BLOCKS_TO_UPLOAD_IN_PARALLEL: usize = 32; + +// Read up to this many blocks from blockstore before blocking on the upload process +const BLOCK_READ_AHEAD_DEPTH: usize = NUM_BLOCKS_TO_UPLOAD_IN_PARALLEL * 2; + +async fn upload( + blockstore: Blockstore, + starting_slot: Slot, + ending_slot: Option, + allow_missing_metadata: bool, +) -> Result<(), Box> { + let mut measure = Measure::start("entire upload"); + + let bigtable = solana_storage_bigtable::LedgerStorage::new(false) + .await + .map_err(|err| format!("Failed to connect to storage: {:?}", err))?; + + info!("Loading ledger slots..."); + let blockstore_slots: Vec<_> = blockstore + .slot_meta_iterator(starting_slot) + .map_err(|err| { + format!( + "Failed to load entries starting from slot {}: {:?}", + starting_slot, err + ) + })? + .filter_map(|(slot, _slot_meta)| { + if let Some(ending_slot) = &ending_slot { + if slot > *ending_slot { + return None; + } + } + Some(slot) + }) + .collect(); + + if blockstore_slots.is_empty() { + info!("Ledger has no slots in the specified range"); + return Ok(()); + } + info!( + "Found {} slots in the range ({}, {})", + blockstore_slots.len(), + blockstore_slots.first().unwrap(), + blockstore_slots.last().unwrap() + ); + + let mut blockstore_slots_with_no_confirmed_block = HashSet::new(); + + // Gather the blocks that are already present in bigtable, by slot + let bigtable_slots = { + let mut bigtable_slots = vec![]; + let first_blockstore_slot = *blockstore_slots.first().unwrap(); + let last_blockstore_slot = *blockstore_slots.last().unwrap(); + info!( + "Loading list of bigtable blocks between slots {} and {}...", + first_blockstore_slot, last_blockstore_slot + ); + + let mut start_slot = *blockstore_slots.first().unwrap(); + while start_slot <= last_blockstore_slot { + let mut next_bigtable_slots = loop { + match bigtable.get_confirmed_blocks(start_slot, 1000).await { + Ok(slots) => break slots, + Err(err) => { + error!("get_confirmed_blocks for {} failed: {:?}", start_slot, err); + // Consider exponential backoff... + delay_for(Duration::from_secs(2)).await; + } + } + }; + if next_bigtable_slots.is_empty() { + break; + } + bigtable_slots.append(&mut next_bigtable_slots); + start_slot = bigtable_slots.last().unwrap() + 1; + } + bigtable_slots + .into_iter() + .filter(|slot| *slot <= last_blockstore_slot) + .collect::>() + }; + + // The blocks that still need to be uploaded is the difference between what's already in the + // bigtable and what's in blockstore... + let blocks_to_upload = { + let blockstore_slots = blockstore_slots.iter().cloned().collect::>(); + let bigtable_slots = bigtable_slots.into_iter().collect::>(); + + let mut blocks_to_upload = blockstore_slots + .difference(&blockstore_slots_with_no_confirmed_block) + .cloned() + .collect::>() + .difference(&bigtable_slots) + .cloned() + .collect::>(); + blocks_to_upload.sort(); + blocks_to_upload + }; + + if blocks_to_upload.is_empty() { + info!("No blocks need to be uploaded to bigtable"); + return Ok(()); + } + info!( + "{} blocks to be uploaded to the bucket in the range ({}, {})", + blocks_to_upload.len(), + blocks_to_upload.first().unwrap(), + blocks_to_upload.last().unwrap() + ); + + // Load the blocks out of blockstore in a separate thread to allow for concurrent block uploading + let (_loader_thread, receiver) = { + let (sender, receiver) = std::sync::mpsc::sync_channel(BLOCK_READ_AHEAD_DEPTH); + ( + std::thread::spawn(move || { + let mut measure = Measure::start("block loader thread"); + for (i, slot) in blocks_to_upload.iter().enumerate() { + let _ = match blockstore.get_confirmed_block( + *slot, + Some(solana_transaction_status::UiTransactionEncoding::Binary), + ) { + Ok(confirmed_block) => sender.send((*slot, Some(confirmed_block))), + Err(err) => { + warn!( + "Failed to get load confirmed block from slot {}: {:?}", + slot, err + ); + sender.send((*slot, None)) + } + }; + + if i % NUM_BLOCKS_TO_UPLOAD_IN_PARALLEL == 0 { + info!( + "{}% of blocks processed ({}/{})", + i * 100 / blocks_to_upload.len(), + i, + blocks_to_upload.len() + ); + } + } + measure.stop(); + info!("{} to load {} blocks", measure, blocks_to_upload.len()); + }), + receiver, + ) + }; + + let mut failures = 0; + use futures::stream::StreamExt; + + let mut stream = + tokio::stream::iter(receiver.into_iter()).chunks(NUM_BLOCKS_TO_UPLOAD_IN_PARALLEL); + + while let Some(blocks) = stream.next().await { + let mut measure_upload = Measure::start("Upload"); + let mut num_blocks = blocks.len(); + info!("Preparing the next {} blocks for upload", num_blocks); + + let uploads = blocks.into_iter().filter_map(|(slot, block)| match block { + None => { + blockstore_slots_with_no_confirmed_block.insert(slot); + num_blocks -= 1; + None + } + Some(confirmed_block) => { + if confirmed_block + .transactions + .iter() + .any(|transaction| transaction.meta.is_none()) + { + if allow_missing_metadata { + info!("Transaction metadata missing from slot {}", slot); + } else { + panic!("Transaction metadata missing from slot {}", slot); + } + } + Some(bigtable.upload_confirmed_block(slot, confirmed_block)) + } + }); + + for result in futures::future::join_all(uploads).await { + if result.is_err() { + error!("upload_confirmed_block() failed: {:?}", result.err()); + failures += 1; + } + } + + measure_upload.stop(); + info!("{} for {} blocks", measure_upload, num_blocks); + } + + measure.stop(); + info!("{}", measure); + if failures > 0 { + Err(format!("Incomplete upload, {} operations failed", failures).into()) + } else { + Ok(()) + } +} + +async fn first_available_block() -> Result<(), Box> { + let bigtable = solana_storage_bigtable::LedgerStorage::new(true).await?; + match bigtable.get_first_available_block().await? { + Some(block) => println!("{}", block), + None => println!("No blocks available"), + } + + Ok(()) +} + +async fn block(slot: Slot) -> Result<(), Box> { + let bigtable = solana_storage_bigtable::LedgerStorage::new(false) + .await + .map_err(|err| format!("Failed to connect to storage: {:?}", err))?; + + let block = bigtable + .get_confirmed_block(slot, UiTransactionEncoding::Binary) + .await?; + + println!("Slot: {}", slot); + println!("Parent Slot: {}", block.parent_slot); + println!("Blockhash: {}", block.blockhash); + println!("Previous Blockhash: {}", block.previous_blockhash); + if block.block_time.is_some() { + println!("Block Time: {:?}", block.block_time); + } + if !block.rewards.is_empty() { + println!("Rewards: {:?}", block.rewards); + } + for (index, transaction_with_meta) in block.transactions.iter().enumerate() { + println!("Transaction {}:", index); + println_transaction( + &transaction_with_meta.transaction.decode().unwrap(), + &transaction_with_meta.meta, + " ", + ); + } + Ok(()) +} + +async fn blocks(starting_slot: Slot, limit: usize) -> Result<(), Box> { + let bigtable = solana_storage_bigtable::LedgerStorage::new(false) + .await + .map_err(|err| format!("Failed to connect to storage: {:?}", err))?; + + let slots = bigtable.get_confirmed_blocks(starting_slot, limit).await?; + println!("{:?}", slots); + println!("{} blocks found", slots.len()); + + Ok(()) +} + +async fn confirm(signature: &Signature, verbose: bool) -> Result<(), Box> { + let bigtable = solana_storage_bigtable::LedgerStorage::new(false) + .await + .map_err(|err| format!("Failed to connect to storage: {:?}", err))?; + + let transaction_status = bigtable.get_signature_status(signature).await?; + + if verbose { + match bigtable + .get_confirmed_transaction(signature, UiTransactionEncoding::Binary) + .await + { + Ok(Some(confirmed_transaction)) => { + println!( + "\nTransaction executed in slot {}:", + confirmed_transaction.slot + ); + println_transaction( + &confirmed_transaction + .transaction + .transaction + .decode() + .expect("Successful decode"), + &confirmed_transaction.transaction.meta, + " ", + ); + } + Ok(None) => println!("Confirmed transaction details not available"), + Err(err) => println!("Unable to get confirmed transaction details: {}", err), + } + println!(); + } + match transaction_status.status { + Ok(_) => println!("Confirmed"), + Err(err) => println!("Transaction failed: {}", err), + } + Ok(()) +} + +pub async fn transaction_history( + address: &Pubkey, + limit: usize, + before: Option<&Signature>, + verbose: bool, +) -> Result<(), Box> { + let bigtable = solana_storage_bigtable::LedgerStorage::new(true).await?; + + let results = bigtable + .get_confirmed_signatures_for_address(address, before, limit) + .await?; + + for (signature, slot, memo, err) in results { + if verbose { + println!( + "{}, slot={}, memo=\"{}\", status={}", + signature, + slot, + memo.unwrap_or_else(|| "".to_string()), + match err { + None => "Confirmed".to_string(), + Some(err) => format!("Failed: {:?}", err), + } + ); + } else { + println!("{}", signature); + } + } + Ok(()) +} + +pub trait BigTableSubCommand { + fn bigtable_subcommand(self) -> Self; +} + +impl BigTableSubCommand for App<'_, '_> { + fn bigtable_subcommand(self) -> Self { + self.subcommand( + SubCommand::with_name("bigtable") + .about("Ledger data on a BigTable instance") + .setting(AppSettings::ArgRequiredElseHelp) + .subcommand( + SubCommand::with_name("upload") + .about("Upload the ledger to BigTable") + .arg( + Arg::with_name("starting_slot") + .long("starting-slot") + .validator(is_slot) + .value_name("SLOT") + .takes_value(true) + .index(1) + .help( + "Start uploading at this slot [default: first available slot]", + ), + ) + .arg( + Arg::with_name("ending_slot") + .long("ending-slot") + .validator(is_slot) + .value_name("SLOT") + .takes_value(true) + .index(2) + .help("Stop uploading at this slot [default: last available slot]"), + ) + .arg( + Arg::with_name("allow_missing_metadata") + .long("allow-missing-metadata") + .takes_value(false) + .help("Don't panic if transaction metadata is missing"), + ), + ) + .subcommand( + SubCommand::with_name("first-available-block") + .about("Get the first available block in the storage"), + ) + .subcommand( + SubCommand::with_name("blocks") + .about("Get a list of slots with confirmed blocks for the given range") + .arg( + Arg::with_name("starting_slot") + .long("starting-slot") + .validator(is_slot) + .value_name("SLOT") + .takes_value(true) + .index(1) + .required(true) + .default_value("0") + .help("Start listing at this slot"), + ) + .arg( + Arg::with_name("limit") + .long("limit") + .validator(is_slot) + .value_name("LIMIT") + .takes_value(true) + .index(2) + .required(true) + .default_value("1000") + .help("Maximum number of slots to return"), + ), + ) + .subcommand( + SubCommand::with_name("block") + .about("Get a confirmed block") + .arg( + Arg::with_name("slot") + .long("slot") + .validator(is_slot) + .value_name("SLOT") + .takes_value(true) + .index(1) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("confirm") + .about("Confirm transaction by signature") + .arg( + Arg::with_name("signature") + .long("signature") + .value_name("TRANSACTION_SIGNATURE") + .takes_value(true) + .required(true) + .index(1) + .help("The transaction signature to confirm"), + ) + .arg( + Arg::with_name("verbose") + .short("v") + .long("verbose") + .takes_value(false) + .help("Show additional information"), + ), + ) + .subcommand( + SubCommand::with_name("transaction-history") + .about( + "Show historical transactions affecting the given address, \ + ordered based on the slot in which they were confirmed in \ + from lowest to highest slot", + ) + .arg( + Arg::with_name("address") + .index(1) + .value_name("ADDRESS") + .required(true) + .validator(is_valid_pubkey) + .help("Account address"), + ) + .arg( + Arg::with_name("limit") + .long("limit") + .takes_value(true) + .value_name("LIMIT") + .validator(is_slot) + .index(2) + .default_value("1000") + .help("Maximum number of transaction signatures to return"), + ) + .arg( + Arg::with_name("before") + .long("before") + .value_name("TRANSACTION_SIGNATURE") + .takes_value(true) + .help("Start with the first signature older than this one"), + ) + .arg( + Arg::with_name("verbose") + .short("v") + .long("verbose") + .takes_value(false) + .help("Show additional information"), + ), + ), + ) + } +} + +pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { + let mut runtime = tokio::runtime::Runtime::new().unwrap(); + + let future = match matches.subcommand() { + ("upload", Some(arg_matches)) => { + let starting_slot = value_t!(arg_matches, "starting_slot", Slot).unwrap_or(0); + let ending_slot = value_t!(arg_matches, "ending_slot", Slot).ok(); + let allow_missing_metadata = arg_matches.is_present("allow_missing_metadata"); + let blockstore = + crate::open_blockstore(&ledger_path, AccessType::TryPrimaryThenSecondary); + + runtime.block_on(upload( + blockstore, + starting_slot, + ending_slot, + allow_missing_metadata, + )) + } + ("first-available-block", Some(_arg_matches)) => runtime.block_on(first_available_block()), + ("block", Some(arg_matches)) => { + let slot = value_t_or_exit!(arg_matches, "slot", Slot); + runtime.block_on(block(slot)) + } + ("blocks", Some(arg_matches)) => { + let starting_slot = value_t_or_exit!(arg_matches, "starting_slot", Slot); + let limit = value_t_or_exit!(arg_matches, "limit", usize); + + runtime.block_on(blocks(starting_slot, limit)) + } + ("confirm", Some(arg_matches)) => { + let signature = arg_matches + .value_of("signature") + .unwrap() + .parse() + .expect("Invalid signature"); + let verbose = arg_matches.is_present("verbose"); + + runtime.block_on(confirm(&signature, verbose)) + } + ("transaction-history", Some(arg_matches)) => { + let address = pubkey_of(arg_matches, "address").unwrap(); + let limit = value_t_or_exit!(arg_matches, "limit", usize); + let before = arg_matches + .value_of("before") + .map(|signature| signature.parse().expect("Invalid signature")); + let verbose = arg_matches.is_present("verbose"); + + runtime.block_on(transaction_history( + &address, + limit, + before.as_ref(), + verbose, + )) + } + _ => unreachable!(), + }; + + future.unwrap_or_else(|err| { + eprintln!("{:?}", err); + exit(1); + }); +} diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index a61159d83b..a0bd769fb0 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -2,6 +2,7 @@ use clap::{ crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App, Arg, ArgMatches, SubCommand, }; +use log::*; use regex::Regex; use serde_json::json; use solana_clap_utils::input_validators::{is_parsable, is_slot}; @@ -40,7 +41,8 @@ use std::{ sync::Arc, }; -use log::*; +mod bigtable; +use bigtable::*; #[derive(PartialEq)] enum LedgerOutputMethod { @@ -704,6 +706,7 @@ fn main() { .global(true) .help("Use DIR for ledger location"), ) + .bigtable_subcommand() .subcommand( SubCommand::with_name("print") .about("Print the ledger") @@ -975,6 +978,7 @@ fn main() { }); match matches.subcommand() { + ("bigtable", Some(arg_matches)) => bigtable_process_command(&ledger_path, arg_matches), ("print", Some(arg_matches)) => { let starting_slot = value_t_or_exit!(arg_matches, "starting_slot", Slot); let allow_dead_slots = arg_matches.is_present("allow_dead_slots"); diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index d87c1407c0..7bb40e7482 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -1652,7 +1652,7 @@ impl Blockstore { iterator .map(|transaction| { let signature = transaction.signatures[0]; - let encoded_transaction = EncodedTransaction::encode(transaction, encoding.clone()); + let encoded_transaction = EncodedTransaction::encode(transaction, encoding); TransactionWithStatusMeta { transaction: encoded_transaction, meta: self diff --git a/multinode-demo/bootstrap-validator.sh b/multinode-demo/bootstrap-validator.sh index e19639384c..b11c041892 100755 --- a/multinode-demo/bootstrap-validator.sh +++ b/multinode-demo/bootstrap-validator.sh @@ -48,6 +48,9 @@ while [[ -n $1 ]]; do elif [[ $1 = --enable-rpc-transaction-history ]]; then args+=("$1") shift + elif [[ $1 = --enable-rpc-bigtable-ledger-storage ]]; then + args+=("$1") + shift elif [[ $1 = --skip-poh-verify ]]; then args+=("$1") shift diff --git a/storage-bigtable/Cargo.toml b/storage-bigtable/Cargo.toml new file mode 100644 index 0000000000..33a3faf3e0 --- /dev/null +++ b/storage-bigtable/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "solana-storage-bigtable" +version = "1.2.28" +description = "Solana Storage BigTable" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +backoff = {version="0.2.1", features = ["tokio"]} +bincode = "1.2.1" +bzip2 = "0.3.3" +enum-iterator = "0.6.0" +flate2 = "1.0.14" +goauth = "0.7.2" +log = "0.4.8" +prost = "0.6.1" +prost-types = "0.6.1" +serde = "1.0.112" +serde_derive = "1.0.103" +smpl_jwt = "0.5.0" +solana-sdk = { path = "../sdk", version = "1.1.20" } +solana-transaction-status = { path = "../transaction-status", version = "1.1.20" } +thiserror = "1.0" +futures = "0.3.5" +tonic = {version="0.3.0", features = ["tls", "transport"]} +zstd = "0.5.1" + +[lib] +crate-type = ["lib"] +name = "solana_storage_bigtable" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/storage-bigtable/README.md b/storage-bigtable/README.md new file mode 100644 index 0000000000..66914bf2a6 --- /dev/null +++ b/storage-bigtable/README.md @@ -0,0 +1,22 @@ + +## BigTable Setup + +### Development Environment +The Cloud BigTable emulator can be used during development/test. See +https://cloud.google.com/bigtable/docs/emulator for general setup information. + +Process: +1. Run `gcloud beta emulators bigtable start` in the background +2. Run `$(gcloud beta emulators bigtable env-init)` to establish the `BIGTABLE_EMULATOR_HOST` environment variable +3. Run `./init-bigtable.sh` to configure the emulator +4. Develop/test + +### Production Environment +Export a standard `GOOGLE_APPLICATION_CREDENTIALS` environment variable to your +service account credentials. The project should contain a BigTable instance +called `solana-ledger` that has been initialized by running the `./init-bigtable.sh` script. + +Depending on what operation mode is required, either the +`https://www.googleapis.com/auth/bigtable.data` or +`https://www.googleapis.com/auth/bigtable.data.readonly` OAuth scope will be +requested using the provided credentials. diff --git a/storage-bigtable/build-proto/.gitignore b/storage-bigtable/build-proto/.gitignore new file mode 100644 index 0000000000..d4c5018754 --- /dev/null +++ b/storage-bigtable/build-proto/.gitignore @@ -0,0 +1,2 @@ +googleapis/ +target/ diff --git a/storage-bigtable/build-proto/Cargo.lock b/storage-bigtable/build-proto/Cargo.lock new file mode 100644 index 0000000000..4780dcb0e9 --- /dev/null +++ b/storage-bigtable/build-proto/Cargo.lock @@ -0,0 +1,340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb" +dependencies = [ + "autocfg", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "indexmap" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "multimap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "prost" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce49aefe0a6144a45de32927c77bd2859a5f7677b55f220ae5b744e87389c212" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b10678c913ecbd69350e8535c3aef91a8676c0773fc1d7b95cdd196d7f2f26" +dependencies = [ + "bytes", + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "proto" +version = "1.1.20" +dependencies = [ + "tonic-build", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tonic-build" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d8d21cb568e802d77055ab7fcd43f0992206de5028de95c8d3a41118d32e8e" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/storage-bigtable/build-proto/Cargo.toml b/storage-bigtable/build-proto/Cargo.toml new file mode 100644 index 0000000000..26d747cdf6 --- /dev/null +++ b/storage-bigtable/build-proto/Cargo.toml @@ -0,0 +1,15 @@ +[package] +authors = ["Solana Maintainers "] +description = "Blockchain, Rebuilt for Scale" +edition = "2018" +homepage = "https://solana.com/" +license = "Apache-2.0" +name = "proto" +publish = false +repository = "https://github.com/solana-labs/solana" +version = "1.1.20" + +[workspace] + +[dependencies] +tonic-build = "0.2.0" diff --git a/storage-bigtable/build-proto/README.md b/storage-bigtable/build-proto/README.md new file mode 100644 index 0000000000..34cf0066bd --- /dev/null +++ b/storage-bigtable/build-proto/README.md @@ -0,0 +1,2 @@ +Helper project to build Rust bindings for BigTable, to avoid requiring all +Solana developers have protoc installed diff --git a/storage-bigtable/build-proto/build.sh b/storage-bigtable/build-proto/build.sh new file mode 100755 index 0000000000..dfc2843727 --- /dev/null +++ b/storage-bigtable/build-proto/build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -ex +cd "$(dirname "$0")" + +if [[ ! -d googleapis ]]; then + git clone https://github.com/googleapis/googleapis.git +fi + +exec cargo run diff --git a/storage-bigtable/build-proto/src/main.rs b/storage-bigtable/build-proto/src/main.rs new file mode 100644 index 0000000000..a61afd1e18 --- /dev/null +++ b/storage-bigtable/build-proto/src/main.rs @@ -0,0 +1,19 @@ +fn main() -> Result<(), std::io::Error> { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + let out_dir = manifest_dir.join("../proto"); + let googleapis = manifest_dir.join("googleapis"); + + println!("Google API directory: {}", googleapis.display()); + println!("output directory: {}", out_dir.display()); + + tonic_build::configure() + .build_client(true) + .build_server(false) + .format(true) + .out_dir(&out_dir) + .compile( + &[googleapis.join("google/bigtable/v2/bigtable.proto")], + &[googleapis], + ) +} diff --git a/storage-bigtable/init-bigtable.sh b/storage-bigtable/init-bigtable.sh new file mode 100755 index 0000000000..3b988e2ef6 --- /dev/null +++ b/storage-bigtable/init-bigtable.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Configures a BigTable instance with the expected tables +# + +set -e + +instance=solana-ledger + +cbt=( + cbt + -instance + "$instance" +) +if [[ -n $BIGTABLE_EMULATOR_HOST ]]; then + cbt+=(-project emulator) +fi + +for table in blocks tx tx-by-addr; do + ( + set -x + "${cbt[@]}" createtable $table + "${cbt[@]}" createfamily $table x + "${cbt[@]}" setgcpolicy $table x maxversions=1 + "${cbt[@]}" setgcpolicy $table x maxage=360d + ) +done diff --git a/storage-bigtable/proto/google.api.rs b/storage-bigtable/proto/google.api.rs new file mode 100644 index 0000000000..a9f7d00700 --- /dev/null +++ b/storage-bigtable/proto/google.api.rs @@ -0,0 +1,632 @@ +/// Defines the HTTP configuration for an API service. It contains a list of +/// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +/// to one or more HTTP REST API methods. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Http { + /// A list of HTTP configuration rules that apply to individual API methods. + /// + /// **NOTE:** All service configuration rules follow "last one wins" order. + #[prost(message, repeated, tag = "1")] + pub rules: ::std::vec::Vec, + /// When set to true, URL path parameters will be fully URI-decoded except in + /// cases of single segment matches in reserved expansion, where "%2F" will be + /// left encoded. + /// + /// The default behavior is to not decode RFC 6570 reserved characters in multi + /// segment matches. + #[prost(bool, tag = "2")] + pub fully_decode_reserved_expansion: bool, +} +/// # gRPC Transcoding +/// +/// gRPC Transcoding is a feature for mapping between a gRPC method and one or +/// more HTTP REST endpoints. It allows developers to build a single API service +/// that supports both gRPC APIs and REST APIs. Many systems, including [Google +/// APIs](https://github.com/googleapis/googleapis), +/// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +/// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +/// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +/// and use it for large scale production services. +/// +/// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +/// how different portions of the gRPC request message are mapped to the URL +/// path, URL query parameters, and HTTP request body. It also controls how the +/// gRPC response message is mapped to the HTTP response body. `HttpRule` is +/// typically specified as an `google.api.http` annotation on the gRPC method. +/// +/// Each mapping specifies a URL path template and an HTTP method. The path +/// template may refer to one or more fields in the gRPC request message, as long +/// as each field is a non-repeated field with a primitive (non-message) type. +/// The path template controls how fields of the request message are mapped to +/// the URL path. +/// +/// Example: +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// get: "/v1/{name=messages/*}" +/// }; +/// } +/// } +/// message GetMessageRequest { +/// string name = 1; // Mapped to URL path. +/// } +/// message Message { +/// string text = 1; // The resource content. +/// } +/// +/// This enables an HTTP REST to gRPC mapping as below: +/// +/// HTTP | gRPC +/// -----|----- +/// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +/// +/// Any fields in the request message which are not bound by the path template +/// automatically become HTTP query parameters if there is no HTTP request body. +/// For example: +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// get:"/v1/messages/{message_id}" +/// }; +/// } +/// } +/// message GetMessageRequest { +/// message SubMessage { +/// string subfield = 1; +/// } +/// string message_id = 1; // Mapped to URL path. +/// int64 revision = 2; // Mapped to URL query parameter `revision`. +/// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +/// } +/// +/// This enables a HTTP JSON to RPC mapping as below: +/// +/// HTTP | gRPC +/// -----|----- +/// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +/// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +/// "foo"))` +/// +/// Note that fields which are mapped to URL query parameters must have a +/// primitive type or a repeated primitive type or a non-repeated message type. +/// In the case of a repeated type, the parameter can be repeated in the URL +/// as `...?param=A¶m=B`. In the case of a message type, each field of the +/// message is mapped to a separate parameter, such as +/// `...?foo.a=A&foo.b=B&foo.c=C`. +/// +/// For HTTP methods that allow a request body, the `body` field +/// specifies the mapping. Consider a REST update method on the +/// message resource collection: +/// +/// service Messaging { +/// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// patch: "/v1/messages/{message_id}" +/// body: "message" +/// }; +/// } +/// } +/// message UpdateMessageRequest { +/// string message_id = 1; // mapped to the URL +/// Message message = 2; // mapped to the body +/// } +/// +/// The following HTTP JSON to RPC mapping is enabled, where the +/// representation of the JSON in the request body is determined by +/// protos JSON encoding: +/// +/// HTTP | gRPC +/// -----|----- +/// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +/// "123456" message { text: "Hi!" })` +/// +/// The special name `*` can be used in the body mapping to define that +/// every field not bound by the path template should be mapped to the +/// request body. This enables the following alternative definition of +/// the update method: +/// +/// service Messaging { +/// rpc UpdateMessage(Message) returns (Message) { +/// option (google.api.http) = { +/// patch: "/v1/messages/{message_id}" +/// body: "*" +/// }; +/// } +/// } +/// message Message { +/// string message_id = 1; +/// string text = 2; +/// } +/// +/// +/// The following HTTP JSON to RPC mapping is enabled: +/// +/// HTTP | gRPC +/// -----|----- +/// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +/// "123456" text: "Hi!")` +/// +/// Note that when using `*` in the body mapping, it is not possible to +/// have HTTP parameters, as all fields not bound by the path end in +/// the body. This makes this option more rarely used in practice when +/// defining REST APIs. The common usage of `*` is in custom methods +/// which don't use the URL at all for transferring data. +/// +/// It is possible to define multiple HTTP methods for one RPC by using +/// the `additional_bindings` option. Example: +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// get: "/v1/messages/{message_id}" +/// additional_bindings { +/// get: "/v1/users/{user_id}/messages/{message_id}" +/// } +/// }; +/// } +/// } +/// message GetMessageRequest { +/// string message_id = 1; +/// string user_id = 2; +/// } +/// +/// This enables the following two alternative HTTP JSON to RPC mappings: +/// +/// HTTP | gRPC +/// -----|----- +/// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +/// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +/// "123456")` +/// +/// ## Rules for HTTP mapping +/// +/// 1. Leaf request fields (recursive expansion nested messages in the request +/// message) are classified into three categories: +/// - Fields referred by the path template. They are passed via the URL path. +/// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +/// request body. +/// - All other fields are passed via the URL query parameters, and the +/// parameter name is the field path in the request message. A repeated +/// field can be represented as multiple query parameters under the same +/// name. +/// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +/// are passed via URL path and HTTP request body. +/// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +/// fields are passed via URL path and URL query parameters. +/// +/// ### Path template syntax +/// +/// Template = "/" Segments [ Verb ] ; +/// Segments = Segment { "/" Segment } ; +/// Segment = "*" | "**" | LITERAL | Variable ; +/// Variable = "{" FieldPath [ "=" Segments ] "}" ; +/// FieldPath = IDENT { "." IDENT } ; +/// Verb = ":" LITERAL ; +/// +/// The syntax `*` matches a single URL path segment. The syntax `**` matches +/// zero or more URL path segments, which must be the last part of the URL path +/// except the `Verb`. +/// +/// The syntax `Variable` matches part of the URL path as specified by its +/// template. A variable template must not contain other variables. If a variable +/// matches a single path segment, its template may be omitted, e.g. `{var}` +/// is equivalent to `{var=*}`. +/// +/// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +/// contains any reserved character, such characters should be percent-encoded +/// before the matching. +/// +/// If a variable contains exactly one path segment, such as `"{var}"` or +/// `"{var=*}"`, when such a variable is expanded into a URL path on the client +/// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +/// server side does the reverse decoding. Such variables show up in the +/// [Discovery +/// Document](https://developers.google.com/discovery/v1/reference/apis) as +/// `{var}`. +/// +/// If a variable contains multiple path segments, such as `"{var=foo/*}"` +/// or `"{var=**}"`, when such a variable is expanded into a URL path on the +/// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +/// The server side does the reverse decoding, except "%2F" and "%2f" are left +/// unchanged. Such variables show up in the +/// [Discovery +/// Document](https://developers.google.com/discovery/v1/reference/apis) as +/// `{+var}`. +/// +/// ## Using gRPC API Service Configuration +/// +/// gRPC API Service Configuration (service config) is a configuration language +/// for configuring a gRPC service to become a user-facing product. The +/// service config is simply the YAML representation of the `google.api.Service` +/// proto message. +/// +/// As an alternative to annotating your proto file, you can configure gRPC +/// transcoding in your service config YAML files. You do this by specifying a +/// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +/// effect as the proto annotation. This can be particularly useful if you +/// have a proto that is reused in multiple services. Note that any transcoding +/// specified in the service config will override any matching transcoding +/// configuration in the proto. +/// +/// Example: +/// +/// http: +/// rules: +/// # Selects a gRPC method and applies HttpRule to it. +/// - selector: example.v1.Messaging.GetMessage +/// get: /v1/messages/{message_id}/{sub.subfield} +/// +/// ## Special notes +/// +/// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +/// proto to JSON conversion must follow the [proto3 +/// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +/// +/// While the single segment variable follows the semantics of +/// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +/// Expansion, the multi segment variable **does not** follow RFC 6570 Section +/// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +/// does not expand special characters like `?` and `#`, which would lead +/// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +/// for multi segment variables. +/// +/// The path variables **must not** refer to any repeated or mapped field, +/// because client libraries are not capable of handling such variable expansion. +/// +/// The path variables **must not** capture the leading "/" character. The reason +/// is that the most common use case "{var}" does not capture the leading "/" +/// character. For consistency, all path variables must share the same behavior. +/// +/// Repeated message fields must not be mapped to URL query parameters, because +/// no client library can support such complicated mapping. +/// +/// If an API needs to use a JSON array for request or response body, it can map +/// the request or response body to a repeated field. However, some gRPC +/// Transcoding implementations may not support this feature. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HttpRule { + /// Selects a method to which this rule applies. + /// + /// Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + #[prost(string, tag = "1")] + pub selector: std::string::String, + /// The name of the request field whose value is mapped to the HTTP request + /// body, or `*` for mapping all request fields not captured by the path + /// pattern to the HTTP body, or omitted for not having any HTTP request body. + /// + /// NOTE: the referred field must be present at the top-level of the request + /// message type. + #[prost(string, tag = "7")] + pub body: std::string::String, + /// Optional. The name of the response field whose value is mapped to the HTTP + /// response body. When omitted, the entire response message will be used + /// as the HTTP response body. + /// + /// NOTE: The referred field must be present at the top-level of the response + /// message type. + #[prost(string, tag = "12")] + pub response_body: std::string::String, + /// Additional HTTP bindings for the selector. Nested bindings must + /// not contain an `additional_bindings` field themselves (that is, + /// the nesting may only be one level deep). + #[prost(message, repeated, tag = "11")] + pub additional_bindings: ::std::vec::Vec, + /// Determines the URL pattern is matched by this rules. This pattern can be + /// used with any of the {get|put|post|delete|patch} methods. A custom method + /// can be defined using the 'custom' field. + #[prost(oneof = "http_rule::Pattern", tags = "2, 3, 4, 5, 6, 8")] + pub pattern: ::std::option::Option, +} +pub mod http_rule { + /// Determines the URL pattern is matched by this rules. This pattern can be + /// used with any of the {get|put|post|delete|patch} methods. A custom method + /// can be defined using the 'custom' field. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Pattern { + /// Maps to HTTP GET. Used for listing and getting information about + /// resources. + #[prost(string, tag = "2")] + Get(std::string::String), + /// Maps to HTTP PUT. Used for replacing a resource. + #[prost(string, tag = "3")] + Put(std::string::String), + /// Maps to HTTP POST. Used for creating a resource or performing an action. + #[prost(string, tag = "4")] + Post(std::string::String), + /// Maps to HTTP DELETE. Used for deleting a resource. + #[prost(string, tag = "5")] + Delete(std::string::String), + /// Maps to HTTP PATCH. Used for updating a resource. + #[prost(string, tag = "6")] + Patch(std::string::String), + /// The custom pattern is used for specifying an HTTP method that is not + /// included in the `pattern` field, such as HEAD, or "*" to leave the + /// HTTP method unspecified for this rule. The wild-card rule is useful + /// for services that provide content to Web (HTML) clients. + #[prost(message, tag = "8")] + Custom(super::CustomHttpPattern), + } +} +/// A custom pattern is used for defining custom HTTP verb. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CustomHttpPattern { + /// The name of this custom HTTP verb. + #[prost(string, tag = "1")] + pub kind: std::string::String, + /// The path matched by this custom verb. + #[prost(string, tag = "2")] + pub path: std::string::String, +} +/// An indicator of the behavior of a given field (for example, that a field +/// is required in requests, or given as output but ignored as input). +/// This **does not** change the behavior in protocol buffers itself; it only +/// denotes the behavior and may affect how API tooling handles the field. +/// +/// Note: This enum **may** receive new values in the future. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum FieldBehavior { + /// Conventional default for enums. Do not use this. + Unspecified = 0, + /// Specifically denotes a field as optional. + /// While all fields in protocol buffers are optional, this may be specified + /// for emphasis if appropriate. + Optional = 1, + /// Denotes a field as required. + /// This indicates that the field **must** be provided as part of the request, + /// and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + Required = 2, + /// Denotes a field as output only. + /// This indicates that the field is provided in responses, but including the + /// field in a request does nothing (the server *must* ignore it and + /// *must not* throw an error as a result of the field's presence). + OutputOnly = 3, + /// Denotes a field as input only. + /// This indicates that the field is provided in requests, and the + /// corresponding field is not included in output. + InputOnly = 4, + /// Denotes a field as immutable. + /// This indicates that the field may be set once in a request to create a + /// resource, but may not be changed thereafter. + Immutable = 5, +} +/// A simple descriptor of a resource type. +/// +/// ResourceDescriptor annotates a resource message (either by means of a +/// protobuf annotation or use in the service config), and associates the +/// resource's schema, the resource type, and the pattern of the resource name. +/// +/// Example: +/// +/// message Topic { +/// // Indicates this message defines a resource schema. +/// // Declares the resource type in the format of {service}/{kind}. +/// // For Kubernetes resources, the format is {api group}/{kind}. +/// option (google.api.resource) = { +/// type: "pubsub.googleapis.com/Topic" +/// name_descriptor: { +/// pattern: "projects/{project}/topics/{topic}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// parent_name_extractor: "projects/{project}" +/// } +/// }; +/// } +/// +/// The ResourceDescriptor Yaml config will look like: +/// +/// resources: +/// - type: "pubsub.googleapis.com/Topic" +/// name_descriptor: +/// - pattern: "projects/{project}/topics/{topic}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// parent_name_extractor: "projects/{project}" +/// +/// Sometimes, resources have multiple patterns, typically because they can +/// live under multiple parents. +/// +/// Example: +/// +/// message LogEntry { +/// option (google.api.resource) = { +/// type: "logging.googleapis.com/LogEntry" +/// name_descriptor: { +/// pattern: "projects/{project}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// parent_name_extractor: "projects/{project}" +/// } +/// name_descriptor: { +/// pattern: "folders/{folder}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Folder" +/// parent_name_extractor: "folders/{folder}" +/// } +/// name_descriptor: { +/// pattern: "organizations/{organization}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Organization" +/// parent_name_extractor: "organizations/{organization}" +/// } +/// name_descriptor: { +/// pattern: "billingAccounts/{billing_account}/logs/{log}" +/// parent_type: "billing.googleapis.com/BillingAccount" +/// parent_name_extractor: "billingAccounts/{billing_account}" +/// } +/// }; +/// } +/// +/// The ResourceDescriptor Yaml config will look like: +/// +/// resources: +/// - type: 'logging.googleapis.com/LogEntry' +/// name_descriptor: +/// - pattern: "projects/{project}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// parent_name_extractor: "projects/{project}" +/// - pattern: "folders/{folder}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Folder" +/// parent_name_extractor: "folders/{folder}" +/// - pattern: "organizations/{organization}/logs/{log}" +/// parent_type: "cloudresourcemanager.googleapis.com/Organization" +/// parent_name_extractor: "organizations/{organization}" +/// - pattern: "billingAccounts/{billing_account}/logs/{log}" +/// parent_type: "billing.googleapis.com/BillingAccount" +/// parent_name_extractor: "billingAccounts/{billing_account}" +/// +/// For flexible resources, the resource name doesn't contain parent names, but +/// the resource itself has parents for policy evaluation. +/// +/// Example: +/// +/// message Shelf { +/// option (google.api.resource) = { +/// type: "library.googleapis.com/Shelf" +/// name_descriptor: { +/// pattern: "shelves/{shelf}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// } +/// name_descriptor: { +/// pattern: "shelves/{shelf}" +/// parent_type: "cloudresourcemanager.googleapis.com/Folder" +/// } +/// }; +/// } +/// +/// The ResourceDescriptor Yaml config will look like: +/// +/// resources: +/// - type: 'library.googleapis.com/Shelf' +/// name_descriptor: +/// - pattern: "shelves/{shelf}" +/// parent_type: "cloudresourcemanager.googleapis.com/Project" +/// - pattern: "shelves/{shelf}" +/// parent_type: "cloudresourcemanager.googleapis.com/Folder" +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceDescriptor { + /// The resource type. It must be in the format of + /// {service_name}/{resource_type_kind}. The `resource_type_kind` must be + /// singular and must not include version numbers. + /// + /// Example: `storage.googleapis.com/Bucket` + /// + /// The value of the resource_type_kind must follow the regular expression + /// /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and + /// should use PascalCase (UpperCamelCase). The maximum number of + /// characters allowed for the `resource_type_kind` is 100. + #[prost(string, tag = "1")] + pub r#type: std::string::String, + /// Optional. The relative resource name pattern associated with this resource + /// type. The DNS prefix of the full resource name shouldn't be specified here. + /// + /// The path pattern must follow the syntax, which aligns with HTTP binding + /// syntax: + /// + /// Template = Segment { "/" Segment } ; + /// Segment = LITERAL | Variable ; + /// Variable = "{" LITERAL "}" ; + /// + /// Examples: + /// + /// - "projects/{project}/topics/{topic}" + /// - "projects/{project}/knowledgeBases/{knowledge_base}" + /// + /// The components in braces correspond to the IDs for each resource in the + /// hierarchy. It is expected that, if multiple patterns are provided, + /// the same component name (e.g. "project") refers to IDs of the same + /// type of resource. + #[prost(string, repeated, tag = "2")] + pub pattern: ::std::vec::Vec, + /// Optional. The field on the resource that designates the resource name + /// field. If omitted, this is assumed to be "name". + #[prost(string, tag = "3")] + pub name_field: std::string::String, + /// Optional. The historical or future-looking state of the resource pattern. + /// + /// Example: + /// + /// // The InspectTemplate message originally only supported resource + /// // names with organization, and project was added later. + /// message InspectTemplate { + /// option (google.api.resource) = { + /// type: "dlp.googleapis.com/InspectTemplate" + /// pattern: + /// "organizations/{organization}/inspectTemplates/{inspect_template}" + /// pattern: "projects/{project}/inspectTemplates/{inspect_template}" + /// history: ORIGINALLY_SINGLE_PATTERN + /// }; + /// } + #[prost(enumeration = "resource_descriptor::History", tag = "4")] + pub history: i32, + /// The plural name used in the resource name and permission names, such as + /// 'projects' for the resource name of 'projects/{project}' and the permission + /// name of 'cloudresourcemanager.googleapis.com/projects.get'. It is the same + /// concept of the `plural` field in k8s CRD spec + /// https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + /// + /// Note: The plural form is required even for singleton resources. See + /// https://aip.dev/156 + #[prost(string, tag = "5")] + pub plural: std::string::String, + /// The same concept of the `singular` field in k8s CRD spec + /// https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + /// Such as "project" for the `resourcemanager.googleapis.com/Project` type. + #[prost(string, tag = "6")] + pub singular: std::string::String, +} +pub mod resource_descriptor { + /// A description of the historical or future-looking state of the + /// resource pattern. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum History { + /// The "unset" value. + Unspecified = 0, + /// The resource originally had one pattern and launched as such, and + /// additional patterns were added later. + OriginallySinglePattern = 1, + /// The resource has one pattern, but the API owner expects to add more + /// later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents + /// that from being necessary once there are multiple patterns.) + FutureMultiPattern = 2, + } +} +/// Defines a proto annotation that describes a string field that refers to +/// an API resource. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceReference { + /// The resource type that the annotated field references. + /// + /// Example: + /// + /// message Subscription { + /// string topic = 2 [(google.api.resource_reference) = { + /// type: "pubsub.googleapis.com/Topic" + /// }]; + /// } + /// + /// Occasionally, a field may reference an arbitrary resource. In this case, + /// APIs use the special value * in their resource reference. + /// + /// Example: + /// + /// message GetIamPolicyRequest { + /// string resource = 2 [(google.api.resource_reference) = { + /// type: "*" + /// }]; + /// } + #[prost(string, tag = "1")] + pub r#type: std::string::String, + /// The resource type of a child collection that the annotated field + /// references. This is useful for annotating the `parent` field that + /// doesn't have a fixed resource type. + /// + /// Example: + /// + /// message ListLogEntriesRequest { + /// string parent = 1 [(google.api.resource_reference) = { + /// child_type: "logging.googleapis.com/LogEntry" + /// }; + /// } + #[prost(string, tag = "2")] + pub child_type: std::string::String, +} diff --git a/storage-bigtable/proto/google.bigtable.v2.rs b/storage-bigtable/proto/google.bigtable.v2.rs new file mode 100644 index 0000000000..7042d5ff5d --- /dev/null +++ b/storage-bigtable/proto/google.bigtable.v2.rs @@ -0,0 +1,1057 @@ +/// Specifies the complete (requested) contents of a single row of a table. +/// Rows which exceed 256MiB in size cannot be read in full. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Row { + /// The unique key which identifies this row within its table. This is the same + /// key that's used to identify the row in, for example, a MutateRowRequest. + /// May contain any non-empty byte string up to 4KiB in length. + #[prost(bytes, tag = "1")] + pub key: std::vec::Vec, + /// May be empty, but only if the entire row is empty. + /// The mutual ordering of column families is not specified. + #[prost(message, repeated, tag = "2")] + pub families: ::std::vec::Vec, +} +/// Specifies (some of) the contents of a single row/column family intersection +/// of a table. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Family { + /// The unique key which identifies this family within its row. This is the + /// same key that's used to identify the family in, for example, a RowFilter + /// which sets its "family_name_regex_filter" field. + /// Must match `[-_.a-zA-Z0-9]+`, except that AggregatingRowProcessors may + /// produce cells in a sentinel family with an empty name. + /// Must be no greater than 64 characters in length. + #[prost(string, tag = "1")] + pub name: std::string::String, + /// Must not be empty. Sorted in order of increasing "qualifier". + #[prost(message, repeated, tag = "2")] + pub columns: ::std::vec::Vec, +} +/// Specifies (some of) the contents of a single row/column intersection of a +/// table. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Column { + /// The unique key which identifies this column within its family. This is the + /// same key that's used to identify the column in, for example, a RowFilter + /// which sets its `column_qualifier_regex_filter` field. + /// May contain any byte string, including the empty string, up to 16kiB in + /// length. + #[prost(bytes, tag = "1")] + pub qualifier: std::vec::Vec, + /// Must not be empty. Sorted in order of decreasing "timestamp_micros". + #[prost(message, repeated, tag = "2")] + pub cells: ::std::vec::Vec, +} +/// Specifies (some of) the contents of a single row/column/timestamp of a table. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Cell { + /// The cell's stored timestamp, which also uniquely identifies it within + /// its column. + /// Values are always expressed in microseconds, but individual tables may set + /// a coarser granularity to further restrict the allowed values. For + /// example, a table which specifies millisecond granularity will only allow + /// values of `timestamp_micros` which are multiples of 1000. + #[prost(int64, tag = "1")] + pub timestamp_micros: i64, + /// The value stored in the cell. + /// May contain any byte string, including the empty string, up to 100MiB in + /// length. + #[prost(bytes, tag = "2")] + pub value: std::vec::Vec, + /// Labels applied to the cell by a [RowFilter][google.bigtable.v2.RowFilter]. + #[prost(string, repeated, tag = "3")] + pub labels: ::std::vec::Vec, +} +/// Specifies a contiguous range of rows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RowRange { + /// The row key at which to start the range. + /// If neither field is set, interpreted as the empty string, inclusive. + #[prost(oneof = "row_range::StartKey", tags = "1, 2")] + pub start_key: ::std::option::Option, + /// The row key at which to end the range. + /// If neither field is set, interpreted as the infinite row key, exclusive. + #[prost(oneof = "row_range::EndKey", tags = "3, 4")] + pub end_key: ::std::option::Option, +} +pub mod row_range { + /// The row key at which to start the range. + /// If neither field is set, interpreted as the empty string, inclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum StartKey { + /// Used when giving an inclusive lower bound for the range. + #[prost(bytes, tag = "1")] + StartKeyClosed(std::vec::Vec), + /// Used when giving an exclusive lower bound for the range. + #[prost(bytes, tag = "2")] + StartKeyOpen(std::vec::Vec), + } + /// The row key at which to end the range. + /// If neither field is set, interpreted as the infinite row key, exclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum EndKey { + /// Used when giving an exclusive upper bound for the range. + #[prost(bytes, tag = "3")] + EndKeyOpen(std::vec::Vec), + /// Used when giving an inclusive upper bound for the range. + #[prost(bytes, tag = "4")] + EndKeyClosed(std::vec::Vec), + } +} +/// Specifies a non-contiguous set of rows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RowSet { + /// Single rows included in the set. + #[prost(bytes, repeated, tag = "1")] + pub row_keys: ::std::vec::Vec>, + /// Contiguous row ranges included in the set. + #[prost(message, repeated, tag = "2")] + pub row_ranges: ::std::vec::Vec, +} +/// Specifies a contiguous range of columns within a single column family. +/// The range spans from <column_family>:<start_qualifier> to +/// <column_family>:<end_qualifier>, where both bounds can be either +/// inclusive or exclusive. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ColumnRange { + /// The name of the column family within which this range falls. + #[prost(string, tag = "1")] + pub family_name: std::string::String, + /// The column qualifier at which to start the range (within `column_family`). + /// If neither field is set, interpreted as the empty string, inclusive. + #[prost(oneof = "column_range::StartQualifier", tags = "2, 3")] + pub start_qualifier: ::std::option::Option, + /// The column qualifier at which to end the range (within `column_family`). + /// If neither field is set, interpreted as the infinite string, exclusive. + #[prost(oneof = "column_range::EndQualifier", tags = "4, 5")] + pub end_qualifier: ::std::option::Option, +} +pub mod column_range { + /// The column qualifier at which to start the range (within `column_family`). + /// If neither field is set, interpreted as the empty string, inclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum StartQualifier { + /// Used when giving an inclusive lower bound for the range. + #[prost(bytes, tag = "2")] + StartQualifierClosed(std::vec::Vec), + /// Used when giving an exclusive lower bound for the range. + #[prost(bytes, tag = "3")] + StartQualifierOpen(std::vec::Vec), + } + /// The column qualifier at which to end the range (within `column_family`). + /// If neither field is set, interpreted as the infinite string, exclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum EndQualifier { + /// Used when giving an inclusive upper bound for the range. + #[prost(bytes, tag = "4")] + EndQualifierClosed(std::vec::Vec), + /// Used when giving an exclusive upper bound for the range. + #[prost(bytes, tag = "5")] + EndQualifierOpen(std::vec::Vec), + } +} +/// Specified a contiguous range of microsecond timestamps. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TimestampRange { + /// Inclusive lower bound. If left empty, interpreted as 0. + #[prost(int64, tag = "1")] + pub start_timestamp_micros: i64, + /// Exclusive upper bound. If left empty, interpreted as infinity. + #[prost(int64, tag = "2")] + pub end_timestamp_micros: i64, +} +/// Specifies a contiguous range of raw byte values. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValueRange { + /// The value at which to start the range. + /// If neither field is set, interpreted as the empty string, inclusive. + #[prost(oneof = "value_range::StartValue", tags = "1, 2")] + pub start_value: ::std::option::Option, + /// The value at which to end the range. + /// If neither field is set, interpreted as the infinite string, exclusive. + #[prost(oneof = "value_range::EndValue", tags = "3, 4")] + pub end_value: ::std::option::Option, +} +pub mod value_range { + /// The value at which to start the range. + /// If neither field is set, interpreted as the empty string, inclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum StartValue { + /// Used when giving an inclusive lower bound for the range. + #[prost(bytes, tag = "1")] + StartValueClosed(std::vec::Vec), + /// Used when giving an exclusive lower bound for the range. + #[prost(bytes, tag = "2")] + StartValueOpen(std::vec::Vec), + } + /// The value at which to end the range. + /// If neither field is set, interpreted as the infinite string, exclusive. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum EndValue { + /// Used when giving an inclusive upper bound for the range. + #[prost(bytes, tag = "3")] + EndValueClosed(std::vec::Vec), + /// Used when giving an exclusive upper bound for the range. + #[prost(bytes, tag = "4")] + EndValueOpen(std::vec::Vec), + } +} +/// Takes a row as input and produces an alternate view of the row based on +/// specified rules. For example, a RowFilter might trim down a row to include +/// just the cells from columns matching a given regular expression, or might +/// return all the cells of a row but not their values. More complicated filters +/// can be composed out of these components to express requests such as, "within +/// every column of a particular family, give just the two most recent cells +/// which are older than timestamp X." +/// +/// There are two broad categories of RowFilters (true filters and transformers), +/// as well as two ways to compose simple filters into more complex ones +/// (chains and interleaves). They work as follows: +/// +/// * True filters alter the input row by excluding some of its cells wholesale +/// from the output row. An example of a true filter is the `value_regex_filter`, +/// which excludes cells whose values don't match the specified pattern. All +/// regex true filters use RE2 syntax (https://github.com/google/re2/wiki/Syntax) +/// in raw byte mode (RE2::Latin1), and are evaluated as full matches. An +/// important point to keep in mind is that `RE2(.)` is equivalent by default to +/// `RE2([^\n])`, meaning that it does not match newlines. When attempting to +/// match an arbitrary byte, you should therefore use the escape sequence `\C`, +/// which may need to be further escaped as `\\C` in your client language. +/// +/// * Transformers alter the input row by changing the values of some of its +/// cells in the output, without excluding them completely. Currently, the only +/// supported transformer is the `strip_value_transformer`, which replaces every +/// cell's value with the empty string. +/// +/// * Chains and interleaves are described in more detail in the +/// RowFilter.Chain and RowFilter.Interleave documentation. +/// +/// The total serialized size of a RowFilter message must not +/// exceed 4096 bytes, and RowFilters may not be nested within each other +/// (in Chains or Interleaves) to a depth of more than 20. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RowFilter { + /// Which of the possible RowFilter types to apply. If none are set, this + /// RowFilter returns all cells in the input row. + #[prost( + oneof = "row_filter::Filter", + tags = "1, 2, 3, 16, 17, 18, 4, 14, 5, 6, 7, 8, 9, 15, 10, 11, 12, 13, 19" + )] + pub filter: ::std::option::Option, +} +pub mod row_filter { + /// A RowFilter which sends rows through several RowFilters in sequence. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Chain { + /// The elements of "filters" are chained together to process the input row: + /// in row -> f(0) -> intermediate row -> f(1) -> ... -> f(N) -> out row + /// The full chain is executed atomically. + #[prost(message, repeated, tag = "1")] + pub filters: ::std::vec::Vec, + } + /// A RowFilter which sends each row to each of several component + /// RowFilters and interleaves the results. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Interleave { + /// The elements of "filters" all process a copy of the input row, and the + /// results are pooled, sorted, and combined into a single output row. + /// If multiple cells are produced with the same column and timestamp, + /// they will all appear in the output row in an unspecified mutual order. + /// Consider the following example, with three filters: + ///```ignore + /// input row + /// | + /// ----------------------------------------------------- + /// | | | + /// f(0) f(1) f(2) + /// | | | + /// 1: foo,bar,10,x foo,bar,10,z far,bar,7,a + /// 2: foo,blah,11,z far,blah,5,x far,blah,5,x + /// | | | + /// ----------------------------------------------------- + /// | + /// 1: foo,bar,10,z // could have switched with #2 + /// 2: foo,bar,10,x // could have switched with #1 + /// 3: foo,blah,11,z + /// 4: far,bar,7,a + /// 5: far,blah,5,x // identical to #6 + /// 6: far,blah,5,x // identical to #5 + /// + /// All interleaved filters are executed atomically. + #[prost(message, repeated, tag = "1")] + pub filters: ::std::vec::Vec, + } + /// A RowFilter which evaluates one of two possible RowFilters, depending on + /// whether or not a predicate RowFilter outputs any cells from the input row. + /// + /// IMPORTANT NOTE: The predicate filter does not execute atomically with the + /// true and false filters, which may lead to inconsistent or unexpected + /// results. Additionally, Condition filters have poor performance, especially + /// when filters are set for the false condition. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Condition { + /// If `predicate_filter` outputs any cells, then `true_filter` will be + /// evaluated on the input row. Otherwise, `false_filter` will be evaluated. + #[prost(message, optional, boxed, tag = "1")] + pub predicate_filter: ::std::option::Option<::std::boxed::Box>, + /// The filter to apply to the input row if `predicate_filter` returns any + /// results. If not provided, no results will be returned in the true case. + #[prost(message, optional, boxed, tag = "2")] + pub true_filter: ::std::option::Option<::std::boxed::Box>, + /// The filter to apply to the input row if `predicate_filter` does not + /// return any results. If not provided, no results will be returned in the + /// false case. + #[prost(message, optional, boxed, tag = "3")] + pub false_filter: ::std::option::Option<::std::boxed::Box>, + } + /// Which of the possible RowFilter types to apply. If none are set, this + /// RowFilter returns all cells in the input row. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Filter { + /// Applies several RowFilters to the data in sequence, progressively + /// narrowing the results. + #[prost(message, tag = "1")] + Chain(Chain), + /// Applies several RowFilters to the data in parallel and combines the + /// results. + #[prost(message, tag = "2")] + Interleave(Interleave), + /// Applies one of two possible RowFilters to the data based on the output of + /// a predicate RowFilter. + #[prost(message, tag = "3")] + Condition(Box), + /// ADVANCED USE ONLY. + /// Hook for introspection into the RowFilter. Outputs all cells directly to + /// the output of the read rather than to any parent filter. Consider the + /// following example: + ///```ignore + /// Chain( + /// FamilyRegex("A"), + /// Interleave( + /// All(), + /// Chain(Label("foo"), Sink()) + /// ), + /// QualifierRegex("B") + /// ) + /// + /// A,A,1,w + /// A,B,2,x + /// B,B,4,z + /// | + /// FamilyRegex("A") + /// | + /// A,A,1,w + /// A,B,2,x + /// | + /// +------------+-------------+ + /// | | + /// All() Label(foo) + /// | | + /// A,A,1,w A,A,1,w,labels:[foo] + /// A,B,2,x A,B,2,x,labels:[foo] + /// | | + /// | Sink() --------------+ + /// | | | + /// +------------+ x------+ A,A,1,w,labels:[foo] + /// | A,B,2,x,labels:[foo] + /// A,A,1,w | + /// A,B,2,x | + /// | | + /// QualifierRegex("B") | + /// | | + /// A,B,2,x | + /// | | + /// +--------------------------------+ + /// | + /// A,A,1,w,labels:[foo] + /// A,B,2,x,labels:[foo] // could be switched + /// A,B,2,x // could be switched + /// + /// Despite being excluded by the qualifier filter, a copy of every cell + /// that reaches the sink is present in the final result. + /// + /// As with an [Interleave][google.bigtable.v2.RowFilter.Interleave], + /// duplicate cells are possible, and appear in an unspecified mutual order. + /// In this case we have a duplicate with column "A:B" and timestamp 2, + /// because one copy passed through the all filter while the other was + /// passed through the label and sink. Note that one copy has label "foo", + /// while the other does not. + /// + /// Cannot be used within the `predicate_filter`, `true_filter`, or + /// `false_filter` of a [Condition][google.bigtable.v2.RowFilter.Condition]. + #[prost(bool, tag = "16")] + Sink(bool), + /// Matches all cells, regardless of input. Functionally equivalent to + /// leaving `filter` unset, but included for completeness. + #[prost(bool, tag = "17")] + PassAllFilter(bool), + /// Does not match any cells, regardless of input. Useful for temporarily + /// disabling just part of a filter. + #[prost(bool, tag = "18")] + BlockAllFilter(bool), + /// Matches only cells from rows whose keys satisfy the given RE2 regex. In + /// other words, passes through the entire row when the key matches, and + /// otherwise produces an empty row. + /// Note that, since row keys can contain arbitrary bytes, the `\C` escape + /// sequence must be used if a true wildcard is desired. The `.` character + /// will not match the new line character `\n`, which may be present in a + /// binary key. + #[prost(bytes, tag = "4")] + RowKeyRegexFilter(std::vec::Vec), + /// Matches all cells from a row with probability p, and matches no cells + /// from the row with probability 1-p. + #[prost(double, tag = "14")] + RowSampleFilter(f64), + /// Matches only cells from columns whose families satisfy the given RE2 + /// regex. For technical reasons, the regex must not contain the `:` + /// character, even if it is not being used as a literal. + /// Note that, since column families cannot contain the new line character + /// `\n`, it is sufficient to use `.` as a full wildcard when matching + /// column family names. + #[prost(string, tag = "5")] + FamilyNameRegexFilter(std::string::String), + /// Matches only cells from columns whose qualifiers satisfy the given RE2 + /// regex. + /// Note that, since column qualifiers can contain arbitrary bytes, the `\C` + /// escape sequence must be used if a true wildcard is desired. The `.` + /// character will not match the new line character `\n`, which may be + /// present in a binary qualifier. + #[prost(bytes, tag = "6")] + ColumnQualifierRegexFilter(std::vec::Vec), + /// Matches only cells from columns within the given range. + #[prost(message, tag = "7")] + ColumnRangeFilter(super::ColumnRange), + /// Matches only cells with timestamps within the given range. + #[prost(message, tag = "8")] + TimestampRangeFilter(super::TimestampRange), + /// Matches only cells with values that satisfy the given regular expression. + /// Note that, since cell values can contain arbitrary bytes, the `\C` escape + /// sequence must be used if a true wildcard is desired. The `.` character + /// will not match the new line character `\n`, which may be present in a + /// binary value. + #[prost(bytes, tag = "9")] + ValueRegexFilter(std::vec::Vec), + /// Matches only cells with values that fall within the given range. + #[prost(message, tag = "15")] + ValueRangeFilter(super::ValueRange), + /// Skips the first N cells of each row, matching all subsequent cells. + /// If duplicate cells are present, as is possible when using an Interleave, + /// each copy of the cell is counted separately. + #[prost(int32, tag = "10")] + CellsPerRowOffsetFilter(i32), + /// Matches only the first N cells of each row. + /// If duplicate cells are present, as is possible when using an Interleave, + /// each copy of the cell is counted separately. + #[prost(int32, tag = "11")] + CellsPerRowLimitFilter(i32), + /// Matches only the most recent N cells within each column. For example, + /// if N=2, this filter would match column `foo:bar` at timestamps 10 and 9, + /// skip all earlier cells in `foo:bar`, and then begin matching again in + /// column `foo:bar2`. + /// If duplicate cells are present, as is possible when using an Interleave, + /// each copy of the cell is counted separately. + #[prost(int32, tag = "12")] + CellsPerColumnLimitFilter(i32), + /// Replaces each cell's value with the empty string. + #[prost(bool, tag = "13")] + StripValueTransformer(bool), + /// Applies the given label to all cells in the output row. This allows + /// the client to determine which results were produced from which part of + /// the filter. + /// + /// Values must be at most 15 characters in length, and match the RE2 + /// pattern `[a-z0-9\\-]+` + /// + /// Due to a technical limitation, it is not currently possible to apply + /// multiple labels to a cell. As a result, a Chain may have no more than + /// one sub-filter which contains a `apply_label_transformer`. It is okay for + /// an Interleave to contain multiple `apply_label_transformers`, as they + /// will be applied to separate copies of the input. This may be relaxed in + /// the future. + #[prost(string, tag = "19")] + ApplyLabelTransformer(std::string::String), + } +} +/// Specifies a particular change to be made to the contents of a row. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Mutation { + /// Which of the possible Mutation types to apply. + #[prost(oneof = "mutation::Mutation", tags = "1, 2, 3, 4")] + pub mutation: ::std::option::Option, +} +pub mod mutation { + /// A Mutation which sets the value of the specified cell. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct SetCell { + /// The name of the family into which new data should be written. + /// Must match `[-_.a-zA-Z0-9]+` + #[prost(string, tag = "1")] + pub family_name: std::string::String, + /// The qualifier of the column into which new data should be written. + /// Can be any byte string, including the empty string. + #[prost(bytes, tag = "2")] + pub column_qualifier: std::vec::Vec, + /// The timestamp of the cell into which new data should be written. + /// Use -1 for current Bigtable server time. + /// Otherwise, the client should set this value itself, noting that the + /// default value is a timestamp of zero if the field is left unspecified. + /// Values must match the granularity of the table (e.g. micros, millis). + #[prost(int64, tag = "3")] + pub timestamp_micros: i64, + /// The value to be written into the specified cell. + #[prost(bytes, tag = "4")] + pub value: std::vec::Vec, + } + /// A Mutation which deletes cells from the specified column, optionally + /// restricting the deletions to a given timestamp range. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct DeleteFromColumn { + /// The name of the family from which cells should be deleted. + /// Must match `[-_.a-zA-Z0-9]+` + #[prost(string, tag = "1")] + pub family_name: std::string::String, + /// The qualifier of the column from which cells should be deleted. + /// Can be any byte string, including the empty string. + #[prost(bytes, tag = "2")] + pub column_qualifier: std::vec::Vec, + /// The range of timestamps within which cells should be deleted. + #[prost(message, optional, tag = "3")] + pub time_range: ::std::option::Option, + } + /// A Mutation which deletes all cells from the specified column family. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct DeleteFromFamily { + /// The name of the family from which cells should be deleted. + /// Must match `[-_.a-zA-Z0-9]+` + #[prost(string, tag = "1")] + pub family_name: std::string::String, + } + /// A Mutation which deletes all cells from the containing row. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct DeleteFromRow {} + /// Which of the possible Mutation types to apply. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Mutation { + /// Set a cell's value. + #[prost(message, tag = "1")] + SetCell(SetCell), + /// Deletes cells from a column. + #[prost(message, tag = "2")] + DeleteFromColumn(DeleteFromColumn), + /// Deletes cells from a column family. + #[prost(message, tag = "3")] + DeleteFromFamily(DeleteFromFamily), + /// Deletes cells from the entire row. + #[prost(message, tag = "4")] + DeleteFromRow(DeleteFromRow), + } +} +/// Specifies an atomic read/modify/write operation on the latest value of the +/// specified column. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReadModifyWriteRule { + /// The name of the family to which the read/modify/write should be applied. + /// Must match `[-_.a-zA-Z0-9]+` + #[prost(string, tag = "1")] + pub family_name: std::string::String, + /// The qualifier of the column to which the read/modify/write should be + /// applied. + /// Can be any byte string, including the empty string. + #[prost(bytes, tag = "2")] + pub column_qualifier: std::vec::Vec, + /// The rule used to determine the column's new latest value from its current + /// latest value. + #[prost(oneof = "read_modify_write_rule::Rule", tags = "3, 4")] + pub rule: ::std::option::Option, +} +pub mod read_modify_write_rule { + /// The rule used to determine the column's new latest value from its current + /// latest value. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Rule { + /// Rule specifying that `append_value` be appended to the existing value. + /// If the targeted cell is unset, it will be treated as containing the + /// empty string. + #[prost(bytes, tag = "3")] + AppendValue(std::vec::Vec), + /// Rule specifying that `increment_amount` be added to the existing value. + /// If the targeted cell is unset, it will be treated as containing a zero. + /// Otherwise, the targeted cell must contain an 8-byte value (interpreted + /// as a 64-bit big-endian signed integer), or the entire request will fail. + #[prost(int64, tag = "4")] + IncrementAmount(i64), + } +} +/// Request message for Bigtable.ReadRows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReadRowsRequest { + /// Required. The unique name of the table from which to read. + /// Values are of the form + /// `projects//instances//tables/`. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "5")] + pub app_profile_id: std::string::String, + /// The row keys and/or ranges to read. If not specified, reads from all rows. + #[prost(message, optional, tag = "2")] + pub rows: ::std::option::Option, + /// The filter to apply to the contents of the specified row(s). If unset, + /// reads the entirety of each row. + #[prost(message, optional, tag = "3")] + pub filter: ::std::option::Option, + /// The read will terminate after committing to N rows' worth of results. The + /// default (zero) is to return all results. + #[prost(int64, tag = "4")] + pub rows_limit: i64, +} +/// Response message for Bigtable.ReadRows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReadRowsResponse { + /// A collection of a row's contents as part of the read request. + #[prost(message, repeated, tag = "1")] + pub chunks: ::std::vec::Vec, + /// Optionally the server might return the row key of the last row it + /// has scanned. The client can use this to construct a more + /// efficient retry request if needed: any row keys or portions of + /// ranges less than this row key can be dropped from the request. + /// This is primarily useful for cases where the server has read a + /// lot of data that was filtered out since the last committed row + /// key, allowing the client to skip that work on a retry. + #[prost(bytes, tag = "2")] + pub last_scanned_row_key: std::vec::Vec, +} +pub mod read_rows_response { + /// Specifies a piece of a row's contents returned as part of the read + /// response stream. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct CellChunk { + /// The row key for this chunk of data. If the row key is empty, + /// this CellChunk is a continuation of the same row as the previous + /// CellChunk in the response stream, even if that CellChunk was in a + /// previous ReadRowsResponse message. + #[prost(bytes, tag = "1")] + pub row_key: std::vec::Vec, + /// The column family name for this chunk of data. If this message + /// is not present this CellChunk is a continuation of the same column + /// family as the previous CellChunk. The empty string can occur as a + /// column family name in a response so clients must check + /// explicitly for the presence of this message, not just for + /// `family_name.value` being non-empty. + #[prost(message, optional, tag = "2")] + pub family_name: ::std::option::Option<::std::string::String>, + /// The column qualifier for this chunk of data. If this message + /// is not present, this CellChunk is a continuation of the same column + /// as the previous CellChunk. Column qualifiers may be empty so + /// clients must check for the presence of this message, not just + /// for `qualifier.value` being non-empty. + #[prost(message, optional, tag = "3")] + pub qualifier: ::std::option::Option<::std::vec::Vec>, + /// The cell's stored timestamp, which also uniquely identifies it + /// within its column. Values are always expressed in + /// microseconds, but individual tables may set a coarser + /// granularity to further restrict the allowed values. For + /// example, a table which specifies millisecond granularity will + /// only allow values of `timestamp_micros` which are multiples of + /// 1000. Timestamps are only set in the first CellChunk per cell + /// (for cells split into multiple chunks). + #[prost(int64, tag = "4")] + pub timestamp_micros: i64, + /// Labels applied to the cell by a + /// [RowFilter][google.bigtable.v2.RowFilter]. Labels are only set + /// on the first CellChunk per cell. + #[prost(string, repeated, tag = "5")] + pub labels: ::std::vec::Vec, + /// The value stored in the cell. Cell values can be split across + /// multiple CellChunks. In that case only the value field will be + /// set in CellChunks after the first: the timestamp and labels + /// will only be present in the first CellChunk, even if the first + /// CellChunk came in a previous ReadRowsResponse. + #[prost(bytes, tag = "6")] + pub value: std::vec::Vec, + /// If this CellChunk is part of a chunked cell value and this is + /// not the final chunk of that cell, value_size will be set to the + /// total length of the cell value. The client can use this size + /// to pre-allocate memory to hold the full cell value. + #[prost(int32, tag = "7")] + pub value_size: i32, + /// Signals to the client concerning previous CellChunks received. + #[prost(oneof = "cell_chunk::RowStatus", tags = "8, 9")] + pub row_status: ::std::option::Option, + } + pub mod cell_chunk { + /// Signals to the client concerning previous CellChunks received. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum RowStatus { + /// Indicates that the client should drop all previous chunks for + /// `row_key`, as it will be re-read from the beginning. + #[prost(bool, tag = "8")] + ResetRow(bool), + /// Indicates that the client can safely process all previous chunks for + /// `row_key`, as its data has been fully read. + #[prost(bool, tag = "9")] + CommitRow(bool), + } + } +} +/// Request message for Bigtable.SampleRowKeys. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SampleRowKeysRequest { + /// Required. The unique name of the table from which to sample row keys. + /// Values are of the form + /// `projects//instances//tables/
`. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "2")] + pub app_profile_id: std::string::String, +} +/// Response message for Bigtable.SampleRowKeys. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SampleRowKeysResponse { + /// Sorted streamed sequence of sample row keys in the table. The table might + /// have contents before the first row key in the list and after the last one, + /// but a key containing the empty string indicates "end of table" and will be + /// the last response given, if present. + /// Note that row keys in this list may not have ever been written to or read + /// from, and users should therefore not make any assumptions about the row key + /// structure that are specific to their use case. + #[prost(bytes, tag = "1")] + pub row_key: std::vec::Vec, + /// Approximate total storage space used by all rows in the table which precede + /// `row_key`. Buffering the contents of all rows between two subsequent + /// samples would require space roughly equal to the difference in their + /// `offset_bytes` fields. + #[prost(int64, tag = "2")] + pub offset_bytes: i64, +} +/// Request message for Bigtable.MutateRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MutateRowRequest { + /// Required. The unique name of the table to which the mutation should be applied. + /// Values are of the form + /// `projects//instances//tables/
`. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "4")] + pub app_profile_id: std::string::String, + /// Required. The key of the row to which the mutation should be applied. + #[prost(bytes, tag = "2")] + pub row_key: std::vec::Vec, + /// Required. Changes to be atomically applied to the specified row. Entries are applied + /// in order, meaning that earlier mutations can be masked by later ones. + /// Must contain at least one entry and at most 100000. + #[prost(message, repeated, tag = "3")] + pub mutations: ::std::vec::Vec, +} +/// Response message for Bigtable.MutateRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MutateRowResponse {} +/// Request message for BigtableService.MutateRows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MutateRowsRequest { + /// Required. The unique name of the table to which the mutations should be applied. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "3")] + pub app_profile_id: std::string::String, + /// Required. The row keys and corresponding mutations to be applied in bulk. + /// Each entry is applied as an atomic mutation, but the entries may be + /// applied in arbitrary order (even between entries for the same row). + /// At least one entry must be specified, and in total the entries can + /// contain at most 100000 mutations. + #[prost(message, repeated, tag = "2")] + pub entries: ::std::vec::Vec, +} +pub mod mutate_rows_request { + /// A mutation for a given row. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Entry { + /// The key of the row to which the `mutations` should be applied. + #[prost(bytes, tag = "1")] + pub row_key: std::vec::Vec, + /// Required. Changes to be atomically applied to the specified row. Mutations are + /// applied in order, meaning that earlier mutations can be masked by + /// later ones. + /// You must specify at least one mutation. + #[prost(message, repeated, tag = "2")] + pub mutations: ::std::vec::Vec, + } +} +/// Response message for BigtableService.MutateRows. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MutateRowsResponse { + /// One or more results for Entries from the batch request. + #[prost(message, repeated, tag = "1")] + pub entries: ::std::vec::Vec, +} +pub mod mutate_rows_response { + /// The result of applying a passed mutation in the original request. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Entry { + /// The index into the original request's `entries` list of the Entry + /// for which a result is being reported. + #[prost(int64, tag = "1")] + pub index: i64, + /// The result of the request Entry identified by `index`. + /// Depending on how requests are batched during execution, it is possible + /// for one Entry to fail due to an error with another Entry. In the event + /// that this occurs, the same error will be reported for both entries. + #[prost(message, optional, tag = "2")] + pub status: ::std::option::Option, + } +} +/// Request message for Bigtable.CheckAndMutateRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CheckAndMutateRowRequest { + /// Required. The unique name of the table to which the conditional mutation should be + /// applied. + /// Values are of the form + /// `projects//instances//tables/
`. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "7")] + pub app_profile_id: std::string::String, + /// Required. The key of the row to which the conditional mutation should be applied. + #[prost(bytes, tag = "2")] + pub row_key: std::vec::Vec, + /// The filter to be applied to the contents of the specified row. Depending + /// on whether or not any results are yielded, either `true_mutations` or + /// `false_mutations` will be executed. If unset, checks that the row contains + /// any values at all. + #[prost(message, optional, tag = "6")] + pub predicate_filter: ::std::option::Option, + /// Changes to be atomically applied to the specified row if `predicate_filter` + /// yields at least one cell when applied to `row_key`. Entries are applied in + /// order, meaning that earlier mutations can be masked by later ones. + /// Must contain at least one entry if `false_mutations` is empty, and at most + /// 100000. + #[prost(message, repeated, tag = "4")] + pub true_mutations: ::std::vec::Vec, + /// Changes to be atomically applied to the specified row if `predicate_filter` + /// does not yield any cells when applied to `row_key`. Entries are applied in + /// order, meaning that earlier mutations can be masked by later ones. + /// Must contain at least one entry if `true_mutations` is empty, and at most + /// 100000. + #[prost(message, repeated, tag = "5")] + pub false_mutations: ::std::vec::Vec, +} +/// Response message for Bigtable.CheckAndMutateRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CheckAndMutateRowResponse { + /// Whether or not the request's `predicate_filter` yielded any results for + /// the specified row. + #[prost(bool, tag = "1")] + pub predicate_matched: bool, +} +/// Request message for Bigtable.ReadModifyWriteRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReadModifyWriteRowRequest { + /// Required. The unique name of the table to which the read/modify/write rules should be + /// applied. + /// Values are of the form + /// `projects//instances//tables/
`. + #[prost(string, tag = "1")] + pub table_name: std::string::String, + /// This value specifies routing for replication. If not specified, the + /// "default" application profile will be used. + #[prost(string, tag = "4")] + pub app_profile_id: std::string::String, + /// Required. The key of the row to which the read/modify/write rules should be applied. + #[prost(bytes, tag = "2")] + pub row_key: std::vec::Vec, + /// Required. Rules specifying how the specified row's contents are to be transformed + /// into writes. Entries are applied in order, meaning that earlier rules will + /// affect the results of later ones. + #[prost(message, repeated, tag = "3")] + pub rules: ::std::vec::Vec, +} +/// Response message for Bigtable.ReadModifyWriteRow. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReadModifyWriteRowResponse { + /// A Row containing the new contents of all cells modified by the request. + #[prost(message, optional, tag = "1")] + pub row: ::std::option::Option, +} +#[doc = r" Generated client implementations."] +pub mod bigtable_client { + #![allow(unused_variables, dead_code, missing_docs)] + use tonic::codegen::*; + #[doc = " Service for reading from and writing to existing Bigtable tables."] + pub struct BigtableClient { + inner: tonic::client::Grpc, + } + impl BigtableClient { + #[doc = r" Attempt to create a new client by connecting to a given endpoint."] + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl BigtableClient + where + T: tonic::client::GrpcService, + T::ResponseBody: Body + HttpBody + Send + 'static, + T::Error: Into, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_interceptor(inner: T, interceptor: impl Into) -> Self { + let inner = tonic::client::Grpc::with_interceptor(inner, interceptor); + Self { inner } + } + #[doc = " Streams back the contents of all requested rows in key order, optionally"] + #[doc = " applying the same Reader filter to each. Depending on their size,"] + #[doc = " rows and cells may be broken up across multiple responses, but"] + #[doc = " atomicity of each row will still be preserved. See the"] + #[doc = " ReadRowsResponse documentation for details."] + pub async fn read_rows( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result>, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/google.bigtable.v2.Bigtable/ReadRows"); + self.inner + .server_streaming(request.into_request(), path, codec) + .await + } + #[doc = " Returns a sample of row keys in the table. The returned row keys will"] + #[doc = " delimit contiguous sections of the table of approximately equal size,"] + #[doc = " which can be used to break up the data for distributed tasks like"] + #[doc = " mapreduces."] + pub async fn sample_row_keys( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/google.bigtable.v2.Bigtable/SampleRowKeys"); + self.inner + .server_streaming(request.into_request(), path, codec) + .await + } + #[doc = " Mutates a row atomically. Cells already present in the row are left"] + #[doc = " unchanged unless explicitly changed by `mutation`."] + pub async fn mutate_row( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/google.bigtable.v2.Bigtable/MutateRow"); + self.inner.unary(request.into_request(), path, codec).await + } + #[doc = " Mutates multiple rows in a batch. Each individual row is mutated"] + #[doc = " atomically as in MutateRow, but the entire batch is not executed"] + #[doc = " atomically."] + pub async fn mutate_rows( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/google.bigtable.v2.Bigtable/MutateRows"); + self.inner + .server_streaming(request.into_request(), path, codec) + .await + } + #[doc = " Mutates a row atomically based on the output of a predicate Reader filter."] + pub async fn check_and_mutate_row( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/google.bigtable.v2.Bigtable/CheckAndMutateRow", + ); + self.inner.unary(request.into_request(), path, codec).await + } + #[doc = " Modifies a row atomically on the server. The method reads the latest"] + #[doc = " existing timestamp and value from the specified columns and writes a new"] + #[doc = " entry based on pre-defined read/modify/write rules. The new value for the"] + #[doc = " timestamp is the greater of the existing timestamp or the current server"] + #[doc = " time. The method returns the new contents of all modified cells."] + pub async fn read_modify_write_row( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/google.bigtable.v2.Bigtable/ReadModifyWriteRow", + ); + self.inner.unary(request.into_request(), path, codec).await + } + } + impl Clone for BigtableClient { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + impl std::fmt::Debug for BigtableClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BigtableClient {{ ... }}") + } + } +} diff --git a/storage-bigtable/proto/google.protobuf.rs b/storage-bigtable/proto/google.protobuf.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/storage-bigtable/proto/google.protobuf.rs @@ -0,0 +1 @@ + diff --git a/storage-bigtable/proto/google.rpc.rs b/storage-bigtable/proto/google.rpc.rs new file mode 100644 index 0000000000..37900a4d58 --- /dev/null +++ b/storage-bigtable/proto/google.rpc.rs @@ -0,0 +1,22 @@ +/// The `Status` type defines a logical error model that is suitable for +/// different programming environments, including REST APIs and RPC APIs. It is +/// used by [gRPC](https://github.com/grpc). Each `Status` message contains +/// three pieces of data: error code, error message, and error details. +/// +/// You can find out more about this error model and how to work with it in the +/// [API Design Guide](https://cloud.google.com/apis/design/errors). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Status { + /// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + #[prost(int32, tag = "1")] + pub code: i32, + /// A developer-facing error message, which should be in English. Any + /// user-facing error message should be localized and sent in the + /// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + #[prost(string, tag = "2")] + pub message: std::string::String, + /// A list of messages that carry the error details. There is a common set of + /// message types for APIs to use. + #[prost(message, repeated, tag = "3")] + pub details: ::std::vec::Vec<::prost_types::Any>, +} diff --git a/storage-bigtable/src/access_token.rs b/storage-bigtable/src/access_token.rs new file mode 100644 index 0000000000..e74dea211c --- /dev/null +++ b/storage-bigtable/src/access_token.rs @@ -0,0 +1,118 @@ +/// A module for managing a Google API access token +use goauth::{ + auth::{JwtClaims, Token}, + credentials::Credentials, +}; +use log::*; +use smpl_jwt::Jwt; +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + {Arc, RwLock}, + }, + time::Instant, +}; + +pub use goauth::scopes::Scope; + +fn load_credentials() -> Result { + // Use standard GOOGLE_APPLICATION_CREDENTIALS environment variable + let credentials_file = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") + .map_err(|_| "GOOGLE_APPLICATION_CREDENTIALS environment variable not found".to_string())?; + + Credentials::from_file(&credentials_file).map_err(|err| { + format!( + "Failed to read GCP credentials from {}: {}", + credentials_file, err + ) + }) +} + +#[derive(Clone)] +pub struct AccessToken { + credentials: Credentials, + scope: Scope, + refresh_active: Arc, + token: Arc>, +} + +impl AccessToken { + pub async fn new(scope: Scope) -> Result { + let credentials = load_credentials()?; + if let Err(err) = credentials.rsa_key() { + Err(format!("Invalid rsa key: {}", err)) + } else { + let token = Arc::new(RwLock::new(Self::get_token(&credentials, &scope).await?)); + let access_token = Self { + credentials, + scope, + token, + refresh_active: Arc::new(AtomicBool::new(false)), + }; + Ok(access_token) + } + } + + /// The project that this token grants access to + pub fn project(&self) -> String { + self.credentials.project() + } + + async fn get_token( + credentials: &Credentials, + scope: &Scope, + ) -> Result<(Token, Instant), String> { + info!("Requesting token for {:?} scope", scope); + let claims = JwtClaims::new( + credentials.iss(), + scope, + credentials.token_uri(), + None, + None, + ); + let jwt = Jwt::new(claims, credentials.rsa_key().unwrap(), None); + + let token = goauth::get_token(&jwt, credentials) + .await + .map_err(|err| format!("Failed to refresh access token: {}", err))?; + + info!("Token expires in {} seconds", token.expires_in()); + Ok((token, Instant::now())) + } + + /// Call this function regularly to ensure the access token does not expire + pub async fn refresh(&self) { + // Check if it's time to try a token refresh + { + let token_r = self.token.read().unwrap(); + if token_r.1.elapsed().as_secs() < token_r.0.expires_in() as u64 / 2 { + return; + } + + if self + .refresh_active + .compare_and_swap(false, true, Ordering::Relaxed) + { + // Refresh already pending + return; + } + } + + info!("Refreshing token"); + let new_token = Self::get_token(&self.credentials, &self.scope).await; + { + let mut token_w = self.token.write().unwrap(); + match new_token { + Ok(new_token) => *token_w = new_token, + Err(err) => warn!("{}", err), + } + self.refresh_active.store(false, Ordering::Relaxed); + } + } + + /// Return an access token suitable for use in an HTTP authorization header + pub fn get(&self) -> String { + let token_r = self.token.read().unwrap(); + format!("{} {}", token_r.0.token_type(), token_r.0.access_token()) + } +} diff --git a/storage-bigtable/src/bigtable.rs b/storage-bigtable/src/bigtable.rs new file mode 100644 index 0000000000..99b25a45b9 --- /dev/null +++ b/storage-bigtable/src/bigtable.rs @@ -0,0 +1,466 @@ +// Primitives for reading/writing BigTable tables + +use crate::access_token::{AccessToken, Scope}; +use crate::compression::{compress_best, decompress}; +use crate::root_ca_certificate; +use log::*; +use thiserror::Error; +use tonic::{metadata::MetadataValue, transport::ClientTlsConfig, Request}; + +mod google { + mod rpc { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + concat!("/proto/google.rpc.rs") + )); + } + pub mod bigtable { + pub mod v2 { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + concat!("/proto/google.bigtable.v2.rs") + )); + } + } +} +use google::bigtable::v2::*; + +pub type RowKey = String; +pub type CellName = String; +pub type CellValue = Vec; +pub type RowData = Vec<(CellName, CellValue)>; + +#[derive(Debug, Error)] +pub enum Error { + #[error("AccessToken error: {0}")] + AccessTokenError(String), + + #[error("Certificate error: {0}")] + CertificateError(String), + + #[error("I/O Error: {0}")] + IoError(std::io::Error), + + #[error("Transport error: {0}")] + TransportError(tonic::transport::Error), + + #[error("Invalid URI {0}: {1}")] + InvalidUri(String, String), + + #[error("Row not found")] + RowNotFound, + + #[error("Row write failed")] + RowWriteFailed, + + #[error("Object not found: {0}")] + ObjectNotFound(String), + + #[error("Object is corrupt: {0}")] + ObjectCorrupt(String), + + #[error("RPC error: {0}")] + RpcError(tonic::Status), +} + +impl std::convert::From for Error { + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} + +impl std::convert::From for Error { + fn from(err: tonic::transport::Error) -> Self { + Self::TransportError(err) + } +} + +impl std::convert::From for Error { + fn from(err: tonic::Status) -> Self { + Self::RpcError(err) + } +} + +pub type Result = std::result::Result; + +#[derive(Clone)] +pub struct BigTableConnection { + access_token: Option, + channel: tonic::transport::Channel, + table_prefix: String, +} + +impl BigTableConnection { + /// Establish a connection to the BigTable instance named `instance_name`. If read-only access + /// is required, the `read_only` flag should be used to reduce the requested OAuth2 scope. + /// + /// The GOOGLE_APPLICATION_CREDENTIALS environment variable will be used to determine the + /// program name that contains the BigTable instance in addition to access credentials. + /// + /// The BIGTABLE_EMULATOR_HOST environment variable is also respected. + /// + pub async fn new(instance_name: &str, read_only: bool) -> Result { + match std::env::var("BIGTABLE_EMULATOR_HOST") { + Ok(endpoint) => { + info!("Connecting to bigtable emulator at {}", endpoint); + + Ok(Self { + access_token: None, + channel: tonic::transport::Channel::from_shared(format!("http://{}", endpoint)) + .map_err(|err| Error::InvalidUri(endpoint, err.to_string()))? + .connect_lazy()?, + table_prefix: format!("projects/emulator/instances/{}/tables/", instance_name), + }) + } + + Err(_) => { + let access_token = AccessToken::new(if read_only { + Scope::BigTableDataReadOnly + } else { + Scope::BigTableData + }) + .await + .map_err(Error::AccessTokenError)?; + + let table_prefix = format!( + "projects/{}/instances/{}/tables/", + access_token.project(), + instance_name + ); + + Ok(Self { + access_token: Some(access_token), + channel: tonic::transport::Channel::from_static( + "https://bigtable.googleapis.com", + ) + .tls_config( + ClientTlsConfig::new() + .ca_certificate( + root_ca_certificate::load().map_err(Error::CertificateError)?, + ) + .domain_name("bigtable.googleapis.com"), + )? + .connect_lazy()?, + table_prefix, + }) + } + } + } + + /// Create a new BigTable client. + /// + /// Clients require `&mut self`, due to `Tonic::transport::Channel` limitations, however + /// creating new clients is cheap and thus can be used as a work around for ease of use. + pub fn client(&self) -> BigTable { + let client = if let Some(access_token) = &self.access_token { + let access_token = access_token.clone(); + bigtable_client::BigtableClient::with_interceptor( + self.channel.clone(), + move |mut req: Request<()>| { + match MetadataValue::from_str(&access_token.get()) { + Ok(authorization_header) => { + req.metadata_mut() + .insert("authorization", authorization_header); + } + Err(err) => { + warn!("Failed to set authorization header: {}", err); + } + } + Ok(req) + }, + ) + } else { + bigtable_client::BigtableClient::new(self.channel.clone()) + }; + BigTable { + access_token: self.access_token.clone(), + client, + table_prefix: self.table_prefix.clone(), + } + } + + pub async fn put_bincode_cells_with_retry( + &self, + table: &str, + cells: &[(RowKey, T)], + ) -> Result + where + T: serde::ser::Serialize, + { + use backoff::{future::FutureOperation as _, ExponentialBackoff}; + (|| async { + let mut client = self.client(); + Ok(client.put_bincode_cells(table, cells).await?) + }) + .retry(ExponentialBackoff::default()) + .await + } +} + +pub struct BigTable { + access_token: Option, + client: bigtable_client::BigtableClient, + table_prefix: String, +} + +impl BigTable { + async fn decode_read_rows_response( + mut rrr: tonic::codec::Streaming, + ) -> Result> { + let mut rows: Vec<(RowKey, RowData)> = vec![]; + + let mut row_key = None; + let mut row_data = vec![]; + + let mut cell_name = None; + let mut cell_timestamp = 0; + let mut cell_value = vec![]; + let mut cell_version_ok = true; + while let Some(res) = rrr.message().await? { + for (i, mut chunk) in res.chunks.into_iter().enumerate() { + // The comments for `read_rows_response::CellChunk` provide essential details for + // understanding how the below decoding works... + trace!("chunk {}: {:?}", i, chunk); + + // Starting a new row? + if !chunk.row_key.is_empty() { + row_key = String::from_utf8(chunk.row_key).ok(); // Require UTF-8 for row keys + } + + // Starting a new cell? + if let Some(qualifier) = chunk.qualifier { + if let Some(cell_name) = cell_name { + row_data.push((cell_name, cell_value)); + cell_value = vec![]; + } + cell_name = String::from_utf8(qualifier).ok(); // Require UTF-8 for cell names + cell_timestamp = chunk.timestamp_micros; + cell_version_ok = true; + } else { + // Continuing the existing cell. Check if this is the start of another version of the cell + if chunk.timestamp_micros != 0 { + if chunk.timestamp_micros < cell_timestamp { + cell_version_ok = false; // ignore older versions of the cell + } else { + // newer version of the cell, remove the older cell + cell_version_ok = true; + cell_value = vec![]; + cell_timestamp = chunk.timestamp_micros; + } + } + } + if cell_version_ok { + cell_value.append(&mut chunk.value); + } + + // End of a row? + if chunk.row_status.is_some() { + if let Some(read_rows_response::cell_chunk::RowStatus::CommitRow(_)) = + chunk.row_status + { + if let Some(cell_name) = cell_name { + row_data.push((cell_name, cell_value)); + } + + if let Some(row_key) = row_key { + rows.push((row_key, row_data)) + } + } + + row_key = None; + row_data = vec![]; + cell_value = vec![]; + cell_name = None; + } + } + } + Ok(rows) + } + + async fn refresh_access_token(&self) { + if let Some(ref access_token) = self.access_token { + access_token.refresh().await; + } + } + + /// Get `table` row keys in lexical order. + /// + /// If `start_at` is provided, the row key listing will start with key. + /// Otherwise the listing will start from the start of the table. + pub async fn get_row_keys( + &mut self, + table_name: &str, + start_at: Option, + rows_limit: i64, + ) -> Result> { + self.refresh_access_token().await; + let response = self + .client + .read_rows(ReadRowsRequest { + table_name: format!("{}{}", self.table_prefix, table_name), + rows_limit, + rows: Some(RowSet { + row_keys: vec![], + row_ranges: if let Some(row_key) = start_at { + vec![RowRange { + start_key: Some(row_range::StartKey::StartKeyClosed( + row_key.into_bytes(), + )), + end_key: None, + }] + } else { + vec![] + }, + }), + filter: Some(RowFilter { + filter: Some(row_filter::Filter::Chain(row_filter::Chain { + filters: vec![ + RowFilter { + // Return minimal number of cells + filter: Some(row_filter::Filter::CellsPerRowLimitFilter(1)), + }, + RowFilter { + // Only return the latest version of each cell + filter: Some(row_filter::Filter::CellsPerColumnLimitFilter(1)), + }, + RowFilter { + // Strip the cell values + filter: Some(row_filter::Filter::StripValueTransformer(true)), + }, + ], + })), + }), + ..ReadRowsRequest::default() + }) + .await? + .into_inner(); + + let rows = Self::decode_read_rows_response(response).await?; + Ok(rows.into_iter().map(|r| r.0).collect()) + } + + /// Get latest data from `limit` rows of `table`, starting inclusively at the `row_key` row. + /// + /// All column families are accepted, and only the latest version of each column cell will be + /// returned. + pub async fn get_row_data(&mut self, table_name: &str, row_key: RowKey) -> Result { + self.refresh_access_token().await; + + let response = self + .client + .read_rows(ReadRowsRequest { + table_name: format!("{}{}", self.table_prefix, table_name), + rows_limit: 1, + rows: Some(RowSet { + row_keys: vec![row_key.into_bytes()], + row_ranges: vec![], + }), + filter: Some(RowFilter { + // Only return the latest version of each cell + filter: Some(row_filter::Filter::CellsPerColumnLimitFilter(1)), + }), + ..ReadRowsRequest::default() + }) + .await? + .into_inner(); + + let rows = Self::decode_read_rows_response(response).await?; + rows.into_iter() + .next() + .map(|r| r.1) + .ok_or_else(|| Error::RowNotFound) + } + + /// Store data for one or more `table` rows in the `family_name` Column family + async fn put_row_data( + &mut self, + table_name: &str, + family_name: &str, + row_data: &[(&RowKey, RowData)], + ) -> Result<()> { + self.refresh_access_token().await; + + let mut entries = vec![]; + for (row_key, row_data) in row_data { + let mutations = row_data + .iter() + .map(|(column_key, column_value)| Mutation { + mutation: Some(mutation::Mutation::SetCell(mutation::SetCell { + family_name: family_name.to_string(), + column_qualifier: column_key.clone().into_bytes(), + timestamp_micros: -1, // server assigned + value: column_value.to_vec(), + })), + }) + .collect(); + + entries.push(mutate_rows_request::Entry { + row_key: (*row_key).clone().into_bytes(), + mutations, + }); + } + + let mut response = self + .client + .mutate_rows(MutateRowsRequest { + table_name: format!("{}{}", self.table_prefix, table_name), + entries, + ..MutateRowsRequest::default() + }) + .await? + .into_inner(); + + while let Some(res) = response.message().await? { + for entry in res.entries { + if let Some(status) = entry.status { + if status.code != 0 { + eprintln!("put_row_data error {}: {}", status.code, status.message); + warn!("put_row_data error {}: {}", status.code, status.message); + return Err(Error::RowWriteFailed); + } + } + } + } + + Ok(()) + } + + pub async fn get_bincode_cell(&mut self, table: &str, key: RowKey) -> Result + where + T: serde::de::DeserializeOwned, + { + let row_data = self.get_row_data(table, key.clone()).await?; + + let value = row_data + .into_iter() + .find(|(name, _)| name == "bin") + .ok_or_else(|| Error::ObjectNotFound(format!("{}/{}", table, key)))? + .1; + + let data = decompress(&value)?; + bincode::deserialize(&data).map_err(|err| { + warn!("Failed to deserialize {}/{}: {}", table, key, err); + Error::ObjectCorrupt(format!("{}/{}", table, key)) + }) + } + + pub async fn put_bincode_cells( + &mut self, + table: &str, + cells: &[(RowKey, T)], + ) -> Result + where + T: serde::ser::Serialize, + { + let mut bytes_written = 0; + let mut new_row_data = vec![]; + for (row_key, data) in cells { + let data = compress_best(&bincode::serialize(&data).unwrap())?; + bytes_written += data.len(); + new_row_data.push((row_key, vec![("bin".to_string(), data)])); + } + + self.put_row_data(table, "x", &new_row_data).await?; + Ok(bytes_written) + } +} diff --git a/storage-bigtable/src/compression.rs b/storage-bigtable/src/compression.rs new file mode 100644 index 0000000000..665f93b469 --- /dev/null +++ b/storage-bigtable/src/compression.rs @@ -0,0 +1,105 @@ +use enum_iterator::IntoEnumIterator; +use std::io::{self, BufReader, Read, Write}; + +#[derive(Debug, Serialize, Deserialize, IntoEnumIterator)] +pub enum CompressionMethod { + NoCompression, + Bzip2, + Gzip, + Zstd, +} + +fn decompress_reader<'a, R: Read + 'a>( + method: CompressionMethod, + stream: R, +) -> Result, io::Error> { + let buf_reader = BufReader::new(stream); + let decompress_reader: Box = match method { + CompressionMethod::Bzip2 => Box::new(bzip2::bufread::BzDecoder::new(buf_reader)), + CompressionMethod::Gzip => Box::new(flate2::read::GzDecoder::new(buf_reader)), + CompressionMethod::Zstd => Box::new(zstd::stream::read::Decoder::new(buf_reader)?), + CompressionMethod::NoCompression => Box::new(buf_reader), + }; + Ok(decompress_reader) +} + +pub fn decompress(data: &[u8]) -> Result, io::Error> { + let method_size = bincode::serialized_size(&CompressionMethod::NoCompression).unwrap(); + if (data.len() as u64) < method_size { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("data len too small: {}", data.len()), + )); + } + let method = bincode::deserialize(&data[..method_size as usize]).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("method deserialize failed: {}", err), + ) + })?; + + let mut reader = decompress_reader(method, &data[method_size as usize..])?; + let mut uncompressed_data = vec![]; + reader.read_to_end(&mut uncompressed_data)?; + Ok(uncompressed_data) +} + +pub fn compress(method: CompressionMethod, data: &[u8]) -> Result, io::Error> { + let mut compressed_data = bincode::serialize(&method).unwrap(); + compressed_data.extend( + match method { + CompressionMethod::Bzip2 => { + let mut e = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::Best); + e.write_all(data)?; + e.finish()? + } + CompressionMethod::Gzip => { + let mut e = + flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(data)?; + e.finish()? + } + CompressionMethod::Zstd => { + let mut e = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap(); + e.write_all(data)?; + e.finish()? + } + CompressionMethod::NoCompression => data.to_vec(), + } + .into_iter(), + ); + + Ok(compressed_data) +} + +pub fn compress_best(data: &[u8]) -> Result, io::Error> { + let mut candidates = vec![]; + for method in CompressionMethod::into_enum_iter() { + candidates.push(compress(method, data)?); + } + + Ok(candidates + .into_iter() + .min_by(|a, b| a.len().cmp(&b.len())) + .unwrap()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_compress_uncompress() { + let data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + assert_eq!( + decompress(&compress_best(&data).expect("compress_best")).expect("decompress"), + data + ); + } + + #[test] + fn test_compress() { + let data = vec![0; 256]; + assert!(compress_best(&data).expect("compress_best").len() < data.len()); + } +} diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs new file mode 100644 index 0000000000..dcee9b117d --- /dev/null +++ b/storage-bigtable/src/lib.rs @@ -0,0 +1,536 @@ +use log::*; +use serde::{Deserialize, Serialize}; +use solana_sdk::{ + clock::{Slot, UnixTimestamp}, + pubkey::Pubkey, + signature::Signature, + sysvar::is_sysvar_id, + transaction::{Transaction, TransactionError}, +}; +use solana_transaction_status::{ + ConfirmedBlock, ConfirmedTransaction, EncodedTransaction, Rewards, TransactionStatus, + TransactionWithStatusMeta, UiTransactionEncoding, UiTransactionStatusMeta, +}; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, +}; +use thiserror::Error; + +#[macro_use] +extern crate serde_derive; + +mod access_token; +mod bigtable; +mod compression; +mod root_ca_certificate; + +#[derive(Debug, Error)] +pub enum Error { + #[error("BigTable: {0}")] + BigTableError(bigtable::Error), + + #[error("I/O Error: {0}")] + IoError(std::io::Error), + + #[error("Transaction encoded is not supported")] + UnsupportedTransactionEncoding, + + #[error("Block not found: {0}")] + BlockNotFound(Slot), + + #[error("Signature not found")] + SignatureNotFound, +} + +impl std::convert::From for Error { + fn from(err: bigtable::Error) -> Self { + Self::BigTableError(err) + } +} + +impl std::convert::From for Error { + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} + +pub type Result = std::result::Result; + +// Convert a slot to its bucket representation whereby lower slots are always lexically ordered +// before higher slots +fn slot_to_key(slot: Slot) -> String { + format!("{:016x}", slot) +} + +// Reverse of `slot_to_key` +fn key_to_slot(key: &str) -> Option { + match Slot::from_str_radix(key, 16) { + Ok(slot) => Some(slot), + Err(err) => { + // bucket data is probably corrupt + warn!("Failed to parse object key as a slot: {}: {}", key, err); + None + } + } +} + +// A serialized `StoredConfirmedBlock` is stored in the `block` table +// +// StoredConfirmedBlock holds the same contents as ConfirmedBlock, but is slightly compressed and avoids +// some serde JSON directives that cause issues with bincode +// +#[derive(Serialize, Deserialize)] +struct StoredConfirmedBlock { + previous_blockhash: String, + blockhash: String, + parent_slot: Slot, + transactions: Vec, + rewards: Rewards, + block_time: Option, +} + +impl StoredConfirmedBlock { + fn into_confirmed_block(self, encoding: UiTransactionEncoding) -> ConfirmedBlock { + let StoredConfirmedBlock { + previous_blockhash, + blockhash, + parent_slot, + transactions, + rewards, + block_time, + } = self; + + ConfirmedBlock { + previous_blockhash, + blockhash, + parent_slot, + transactions: transactions + .into_iter() + .map(|transaction| transaction.into_transaction_with_status_meta(encoding)) + .collect(), + rewards, + block_time, + } + } +} + +impl TryFrom for StoredConfirmedBlock { + type Error = Error; + + fn try_from(confirmed_block: ConfirmedBlock) -> Result { + let ConfirmedBlock { + previous_blockhash, + blockhash, + parent_slot, + transactions, + rewards, + block_time, + } = confirmed_block; + + let mut encoded_transactions = vec![]; + for transaction in transactions.into_iter() { + encoded_transactions.push(transaction.try_into()?); + } + + Ok(Self { + previous_blockhash, + blockhash, + parent_slot, + transactions: encoded_transactions, + rewards, + block_time, + }) + } +} + +#[derive(Serialize, Deserialize)] +struct StoredConfirmedBlockTransaction { + transaction: Transaction, + meta: Option, +} + +impl StoredConfirmedBlockTransaction { + fn into_transaction_with_status_meta( + self, + encoding: UiTransactionEncoding, + ) -> TransactionWithStatusMeta { + let StoredConfirmedBlockTransaction { transaction, meta } = self; + TransactionWithStatusMeta { + transaction: EncodedTransaction::encode(transaction, encoding), + meta: meta.map(|meta| meta.into()), + } + } +} + +impl TryFrom for StoredConfirmedBlockTransaction { + type Error = Error; + + fn try_from(value: TransactionWithStatusMeta) -> Result { + let TransactionWithStatusMeta { transaction, meta } = value; + + Ok(Self { + transaction: transaction + .decode() + .ok_or(Error::UnsupportedTransactionEncoding)?, + meta: meta.map(|meta| meta.into()), + }) + } +} + +#[derive(Serialize, Deserialize)] +struct StoredConfirmedBlockTransactionStatusMeta { + err: Option, + fee: u64, + pre_balances: Vec, + post_balances: Vec, +} + +impl From for UiTransactionStatusMeta { + fn from(value: StoredConfirmedBlockTransactionStatusMeta) -> Self { + let StoredConfirmedBlockTransactionStatusMeta { + err, + fee, + pre_balances, + post_balances, + } = value; + let status = match &err { + None => Ok(()), + Some(err) => Err(err.clone()), + }; + Self { + err, + status, + fee, + pre_balances, + post_balances, + } + } +} + +impl From for StoredConfirmedBlockTransactionStatusMeta { + fn from(value: UiTransactionStatusMeta) -> Self { + let UiTransactionStatusMeta { + err, + fee, + pre_balances, + post_balances, + .. + } = value; + Self { + err, + fee, + pre_balances, + post_balances, + } + } +} + +// A serialized `TransactionInfo` is stored in the `tx` table +#[derive(Serialize, Deserialize)] +struct TransactionInfo { + slot: Slot, // The slot that contains the block with this transaction in it + index: u32, // Where the transaction is located in the block + err: Option, // None if the transaction executed successfully + memo: Option, // Transaction memo +} + +impl From for TransactionStatus { + fn from(transaction_info: TransactionInfo) -> Self { + let TransactionInfo { slot, err, .. } = transaction_info; + let status = match &err { + None => Ok(()), + Some(err) => Err(err.clone()), + }; + Self { + slot, + confirmations: None, + status, + err, + } + } +} + +// A serialized `Vec` is stored in the `tx-by-addr` table. The row keys are +// the one's compliment of the slot so that rows may be listed in reverse order +#[derive(Serialize, Deserialize)] +struct TransactionByAddrInfo { + signature: Signature, // The transaction signature + err: Option, // None if the transaction executed successfully + index: u32, // Where the transaction is located in the block + memo: Option, // Transaction memo +} + +#[derive(Clone)] +pub struct LedgerStorage { + connection: bigtable::BigTableConnection, +} + +impl LedgerStorage { + pub async fn new(read_only: bool) -> Result { + let connection = bigtable::BigTableConnection::new("solana-ledger", read_only).await?; + Ok(Self { connection }) + } + + /// Return the available slot that contains a block + pub async fn get_first_available_block(&self) -> Result> { + let mut bigtable = self.connection.client(); + let blocks = bigtable.get_row_keys("blocks", None, 1).await?; + if blocks.is_empty() { + return Ok(None); + } + Ok(key_to_slot(&blocks[0])) + } + + /// Fetch the next slots after the provided slot that contains a block + /// + /// start_slot: slot to start the search from (inclusive) + /// limit: stop after this many slots have been found. + pub async fn get_confirmed_blocks(&self, start_slot: Slot, limit: usize) -> Result> { + let mut bigtable = self.connection.client(); + let blocks = bigtable + .get_row_keys("blocks", Some(slot_to_key(start_slot)), limit as i64) + .await?; + Ok(blocks.into_iter().filter_map(|s| key_to_slot(&s)).collect()) + } + + /// Fetch the confirmed block from the desired slot + pub async fn get_confirmed_block( + &self, + slot: Slot, + encoding: UiTransactionEncoding, + ) -> Result { + let mut bigtable = self.connection.client(); + let block = bigtable + .get_bincode_cell::("blocks", slot_to_key(slot)) + .await?; + Ok(block.into_confirmed_block(encoding)) + } + + pub async fn get_signature_status(&self, signature: &Signature) -> Result { + let mut bigtable = self.connection.client(); + let transaction_info = bigtable + .get_bincode_cell::("tx", signature.to_string()) + .await?; + Ok(transaction_info.into()) + } + + /// Fetch a confirmed transaction + pub async fn get_confirmed_transaction( + &self, + signature: &Signature, + encoding: UiTransactionEncoding, + ) -> Result> { + let mut bigtable = self.connection.client(); + + // Figure out which block the transaction is located in + let TransactionInfo { slot, index, .. } = bigtable + .get_bincode_cell("tx", signature.to_string()) + .await?; + + // Load the block and return the transaction + let block = bigtable + .get_bincode_cell::("blocks", slot_to_key(slot)) + .await?; + match block.transactions.into_iter().nth(index as usize) { + None => { + warn!("Transaction info for {} is corrupt", signature); + Ok(None) + } + Some(bucket_block_transaction) => { + if bucket_block_transaction.transaction.signatures[0] != *signature { + warn!( + "Transaction info or confirmed block for {} is corrupt", + signature + ); + Ok(None) + } else { + Ok(Some(ConfirmedTransaction { + slot, + transaction: bucket_block_transaction + .into_transaction_with_status_meta(encoding), + })) + } + } + } + } + + /// Get confirmed signatures for the provided address, in descending ledger order + /// + /// address: address to search for + /// before_signature: start with the first signature older than this one + /// limit: stop after this many signatures. + pub async fn get_confirmed_signatures_for_address( + &self, + address: &Pubkey, + before_signature: Option<&Signature>, + limit: usize, + ) -> Result, Option)>> { + let mut bigtable = self.connection.client(); + let address_prefix = format!("{}/", address); + + // Figure out where to start listing from based on `before_signature` + let (first_slot, mut first_transaction_index) = match before_signature { + None => (Slot::MAX, 0), + Some(before_signature) => { + let TransactionInfo { slot, index, .. } = bigtable + .get_bincode_cell("tx", before_signature.to_string()) + .await?; + + (slot, index + 1) + } + }; + + let mut infos = vec![]; + + // Return the next `limit` tx-by-addr keys + let tx_by_addr_info_keys = bigtable + .get_row_keys( + "tx-by-addr", + Some(format!("{}{}", address_prefix, slot_to_key(!first_slot))), + limit as i64, + ) + .await?; + + // Read each tx-by-addr object until `limit` signatures have been found + 'outer: for key in tx_by_addr_info_keys { + trace!("key is {}: slot is {}", key, &key[address_prefix.len()..]); + if !key.starts_with(&address_prefix) { + break 'outer; + } + + let slot = !key_to_slot(&key[address_prefix.len()..]).ok_or_else(|| { + bigtable::Error::ObjectCorrupt(format!( + "Failed to convert key to slot: tx-by-addr/{}", + key + )) + })?; + + let tx_by_addr_infos = bigtable + .get_bincode_cell::>("tx-by-addr", key) + .await?; + + for tx_by_addr_info in tx_by_addr_infos + .into_iter() + .skip(first_transaction_index as usize) + { + infos.push(( + tx_by_addr_info.signature, + slot, + tx_by_addr_info.memo, + tx_by_addr_info.err, + )); + if infos.len() >= limit { + break 'outer; + } + } + + first_transaction_index = 0; + } + Ok(infos) + } + + // Upload a new confirmed block and associated meta data. + pub async fn upload_confirmed_block( + &self, + slot: Slot, + confirmed_block: ConfirmedBlock, + ) -> Result<()> { + let mut bytes_written = 0; + + let mut by_addr: HashMap> = HashMap::new(); + + let mut tx_cells = vec![]; + for (index, transaction_with_meta) in confirmed_block.transactions.iter().enumerate() { + let err = transaction_with_meta + .meta + .as_ref() + .and_then(|meta| meta.err.clone()); + let index = index as u32; + let transaction = transaction_with_meta + .transaction + .decode() + .expect("transaction decode failed"); + let signature = transaction.signatures[0]; + + for address in transaction.message.account_keys { + if !is_sysvar_id(&address) { + by_addr + .entry(address) + .or_default() + .push(TransactionByAddrInfo { + signature, + err: err.clone(), + index, + memo: None, // TODO + }); + } + } + + tx_cells.push(( + signature.to_string(), + TransactionInfo { + slot, + index, + err, + memo: None, // TODO + }, + )); + } + + let tx_by_addr_cells: Vec<_> = by_addr + .into_iter() + .map(|(address, transaction_info_by_addr)| { + ( + format!("{}/{}", address, slot_to_key(!slot)), + transaction_info_by_addr, + ) + }) + .collect(); + + if !tx_cells.is_empty() { + bytes_written += self + .connection + .put_bincode_cells_with_retry::("tx", &tx_cells) + .await?; + } + + if !tx_by_addr_cells.is_empty() { + bytes_written += self + .connection + .put_bincode_cells_with_retry::>( + "tx-by-addr", + &tx_by_addr_cells, + ) + .await?; + } + + let num_transactions = confirmed_block.transactions.len(); + + // Store the block itself last, after all other metadata about the block has been + // successfully stored. This avoids partial uploaded blocks from becoming visible to + // `get_confirmed_block()` and `get_confirmed_blocks()` + let blocks_cells = [(slot_to_key(slot), confirmed_block.try_into()?)]; + bytes_written += self + .connection + .put_bincode_cells_with_retry::("blocks", &blocks_cells) + .await?; + info!( + "uploaded block for slot {}: {} transactions, {} bytes", + slot, num_transactions, bytes_written + ); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_slot_to_key() { + assert_eq!(slot_to_key(0), "0000000000000000"); + assert_eq!(slot_to_key(!0), "ffffffffffffffff"); + } +} diff --git a/storage-bigtable/src/pki-goog-roots.pem b/storage-bigtable/src/pki-goog-roots.pem new file mode 100644 index 0000000000..0a6808dba8 --- /dev/null +++ b/storage-bigtable/src/pki-goog-roots.pem @@ -0,0 +1,1222 @@ +# Operating CA: DigiCert +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Operating CA: DigiCert +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - EC1" +# Serial: 51543124481930649114116133369 +# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc +# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47 +# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G2" +# Serial: 1246989352 +# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2 +# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4 +# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946069240 +# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90 +# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31 +# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Operating CA: Entrust Datacard +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Operating CA: GlobalSign +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Operating CA: GlobalSign +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Operating CA: GlobalSign +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Operating CA: GlobalSign +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Note: "GlobalSign Root CA - R7" not added on purpose. It is P-521. + +# Operating CA: GoDaddy +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Operating CA: GoDaddy +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Operating CA: GoDaddy +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Operating CA: GoDaddy +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 14367148294922964480859022125800977897474 +# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e +# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb +# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R1 +# Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R1 +# Label: "GTS Root R1" +# Serial: 6e:47:a9:c5:4b:47:0c:0d:ec:33:d0:89:b9:1c:f4:e1 +# MD5 Fingerprint: 82:1A:EF:D4:D2:4A:F2:9F:E2:3D:97:06:14:70:72:85 +# SHA1 Fingerprint: E1:C9:50:E6:EF:22:F8:4C:56:45:72:8B:92:20:60:D7:D5:A7:A3:E8 +# SHA256 Fingerprint: 2A:57:54:71:E3:13:40:BC:21:58:1C:BD:2C:F1:3E:15:84:63:20:3E:CE:94:BC:F9:D3:CC:19:6B:F0:9A:54:72 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy +MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl +cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM +f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX +mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7 +zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P +fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc +vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4 +Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp +zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO +Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW +k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+ +DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF +lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW +Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z +XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR +gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3 +d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv +J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg +DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM ++SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy +F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9 +SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws +E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R2 +# Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R2 +# Label: "GTS Root R2" +# Serial: 6e:47:a9:c6:5a:b3:e7:20:c5:30:9a:3f:68:52:f2:6f +# MD5 Fingerprint: 44:ED:9A:0E:A4:09:3B:00:F2:AE:4C:A3:C6:61:B0:8B +# SHA1 Fingerprint: D2:73:96:2A:2A:5E:39:9F:73:3F:E1:C7:1E:64:3F:03:38:34:FC:4D +# SHA256 Fingerprint: C4:5D:7B:B0:8E:6D:67:E6:2E:42:35:11:0B:56:4E:5F:78:FD:92:EF:05:8C:84:0A:EA:4E:64:55:D7:58:5C:60 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy +MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl +cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv +CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg +GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu +XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd +re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu +PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1 +mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K +8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj +x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR +nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0 +kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok +twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp +8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT +z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA +pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb +pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB +R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R +RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk +0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC +5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF +izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn +yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R3 +# Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R3 +# Label: "GTS Root R3" +# Serial: 6e:47:a9:c7:6c:a9:73:24:40:89:0f:03:55:dd:8d:1d +# MD5 Fingerprint: 1A:79:5B:6B:04:52:9C:5D:C7:74:33:1B:25:9A:F9:25 +# SHA1 Fingerprint: 30:D4:24:6F:07:FF:DB:91:89:8A:0B:E9:49:66:11:EB:8C:5E:46:E5 +# SHA256 Fingerprint: 15:D5:B8:77:46:19:EA:7D:54:CE:1C:A6:D0:B0:C4:03:E0:37:A9:17:F1:31:E8:A0:4E:1E:6B:7A:71:BA:BC:E5 +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout +736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A +DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk +fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA +njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +# Operating CA: Google Trust Services LLC +# Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R4 +# Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R4 +# Label: "GTS Root R4" +# Serial: 6e:47:a9:c8:8b:94:b6:e8:bb:3b:2a:d8:a2:b2:c1:99 +# MD5 Fingerprint: 5D:B6:6A:C4:60:17:24:6A:1A:99:A8:4B:EE:5E:B4:26 +# SHA1 Fingerprint: 2A:1D:60:27:D9:4A:B1:0A:1C:4D:91:5C:CD:33:A0:CB:3E:2D:54:CB +# SHA256 Fingerprint: 71:CC:A5:39:1F:9E:79:4B:04:80:25:30:B3:63:E1:21:DA:8A:30:43:BB:26:66:2F:EA:4D:CA:7F:C9:51:A4:BD +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu +hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l +xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0 +CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx +sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Operating CA: Sectigo +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- diff --git a/storage-bigtable/src/root_ca_certificate.rs b/storage-bigtable/src/root_ca_certificate.rs new file mode 100644 index 0000000000..e0d43231d4 --- /dev/null +++ b/storage-bigtable/src/root_ca_certificate.rs @@ -0,0 +1,20 @@ +use std::{fs::File, io::Read}; +use tonic::transport::Certificate; + +pub fn load() -> Result { + // Respect the standard GRPC_DEFAULT_SSL_ROOTS_FILE_PATH environment variable if present, + // otherwise use the built-in root certificate + let pem = match std::env::var("GRPC_DEFAULT_SSL_ROOTS_FILE_PATH").ok() { + Some(cert_file) => File::open(&cert_file) + .and_then(|mut file| { + let mut pem = Vec::new(); + file.read_to_end(&mut pem).map(|_| pem) + }) + .map_err(|err| format!("Failed to read {}: {}", cert_file, err))?, + None => { + // PEM file from Google Trust Services (https://pki.goog/roots.pem) + include_bytes!("pki-goog-roots.pem").to_vec() + } + }; + Ok(Certificate::from_pem(&pem)) +} diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 68fde844fb..6044ee1193 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -214,7 +214,7 @@ pub struct TransactionWithStatusMeta { pub meta: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub enum UiTransactionEncoding { Binary, diff --git a/validator/src/main.rs b/validator/src/main.rs index e868562af7..7f5f5c0271 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -620,7 +620,15 @@ pub fn main() { .takes_value(false) .help("Enable historical transaction info over JSON RPC, \ including the 'getConfirmedBlock' API. \ - This will cause an increase in disk usage and IOPS"), + This will cause an increase in disk usage and IOPS"), + ) + .arg( + Arg::with_name("enable_rpc_bigtable_ledger_storage") + .long("enable-rpc-bigtable-ledger-storage") + .requires("enable_rpc_transaction_history") + .takes_value(false) + .help("Fetch historical transaction info from a BigTable instance \ + as a fallback to local ledger data"), ) .arg( Arg::with_name("health_check_slot_distance") @@ -914,6 +922,8 @@ pub fn main() { enable_validator_exit: matches.is_present("enable_rpc_exit"), enable_set_log_filter: matches.is_present("enable_rpc_set_log_filter"), enable_rpc_transaction_history: matches.is_present("enable_rpc_transaction_history"), + enable_bigtable_ledger_storage: matches + .is_present("enable_rpc_bigtable_ledger_storage"), identity_pubkey: identity_keypair.pubkey(), faucet_addr: matches.value_of("rpc_faucet_addr").map(|address| { solana_net_utils::parse_host_port(address).expect("failed to parse faucet address")