From 1bb06324bc741d005cc2165c58ec7be6340d37a9 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 21 Sep 2022 16:56:11 +0100 Subject: [PATCH] Use anonymous client for reading (#1100) --- .../src/types/checkpoint_syncer.rs | 9 +-- rust/abacus-base/src/types/s3_storage.rs | 70 ++++++++++++++----- typescript/infra/scripts/verify-validator.ts | 5 +- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/rust/abacus-base/src/types/checkpoint_syncer.rs b/rust/abacus-base/src/types/checkpoint_syncer.rs index db59ee801..1b4981f69 100644 --- a/rust/abacus-base/src/types/checkpoint_syncer.rs +++ b/rust/abacus-base/src/types/checkpoint_syncer.rs @@ -40,11 +40,11 @@ impl CheckpointSyncerConf { LocalStorage::new(path, latest_index_gauge), )), CheckpointSyncerConf::S3 { bucket, region } => { - Ok(CheckpointSyncers::S3(S3Storage::new( + Ok(CheckpointSyncers::S3(Box::new(S3Storage::new( bucket, region.parse().expect("invalid s3 region"), latest_index_gauge, - ))) + )))) } } } @@ -88,8 +88,9 @@ impl MultisigCheckpointSyncerConf { pub enum CheckpointSyncers { /// A local checkpoint syncer Local(LocalStorage), - /// A checkpoint syncer on s3 - S3(S3Storage), + /// A checkpoint syncer on S3. + /// Boxed due to large size difference between variants. + S3(Box), } #[async_trait] diff --git a/rust/abacus-base/src/types/s3_storage.rs b/rust/abacus-base/src/types/s3_storage.rs index 526cfd15a..f96f425b0 100644 --- a/rust/abacus-base/src/types/s3_storage.rs +++ b/rust/abacus-base/src/types/s3_storage.rs @@ -4,8 +4,12 @@ use abacus_core::SignedCheckpoint; use async_trait::async_trait; use eyre::{bail, Result}; use futures_util::TryStreamExt; +use once_cell::sync::OnceCell; use prometheus::IntGauge; -use rusoto_core::{credential::EnvironmentProvider, HttpClient, Region, RusotoError}; +use rusoto_core::{ + credential::{Anonymous, AwsCredentials, EnvironmentProvider, StaticProvider}, + HttpClient, Region, RusotoError, +}; use rusoto_s3::{GetObjectError, GetObjectRequest, PutObjectRequest, S3Client, S3}; use crate::CheckpointSyncer; @@ -13,12 +17,15 @@ use crate::CheckpointSyncer; #[derive(Clone)] /// Type for reading/writing to S3 pub struct S3Storage { - /// bucket + /// The name of the bucket. bucket: String, - /// region + /// The region of the bucket. region: Region, - /// client - client: S3Client, + /// A client with AWS credentials. + authenticated_client: OnceCell, + /// A client without credentials for anonymous requests. + anonymous_client: OnceCell, + /// The latest seen signed checkpoint index. latest_index: Option, } @@ -34,16 +41,11 @@ impl fmt::Debug for S3Storage { impl S3Storage { /// constructor pub fn new(bucket: &str, region: Region, latest_index: Option) -> Self { - let client = S3Client::new_with( - HttpClient::new().unwrap(), - EnvironmentProvider::default(), - region.clone(), - ); - Self { bucket: bucket.to_owned(), region, - client, + authenticated_client: OnceCell::new(), + anonymous_client: OnceCell::new(), latest_index, } } @@ -56,17 +58,18 @@ impl S3Storage { content_type: Some("application/json".to_owned()), ..Default::default() }; - self.client.put_object(req).await?; + self.authenticated_client().put_object(req).await?; Ok(()) } - async fn read_from_bucket(&self, key: String) -> Result>> { + /// Uses an anonymous client. This should only be used for publicly accessible buckets. + async fn anonymously_read_from_bucket(&self, key: String) -> Result>> { let req = GetObjectRequest { key, bucket: self.bucket.clone(), ..Default::default() }; - match self.client.get_object(req).await { + match self.anonymous_client().get_object(req).await { Ok(res) => match res.body { Some(body) => Ok(Some(body.map_ok(|b| b.to_vec()).try_concat().await?)), None => Ok(None), @@ -76,6 +79,37 @@ impl S3Storage { } } + /// Gets an authenticated S3Client, creating it if it doesn't already exist. + fn authenticated_client(&self) -> &S3Client { + self.authenticated_client.get_or_init(|| { + S3Client::new_with( + HttpClient::new().unwrap(), + EnvironmentProvider::default(), + self.region.clone(), + ) + }) + } + + /// Gets an anonymous S3Client, creating it if it doesn't already exist. + /// An anonymous client doesn't have AWS credentials and will not sign S3 + /// requests with any credentials. + /// We've experienced an inability to make GetObjectRequests to public + /// S3 buckets when signing with credentials from an AWS account not from the + /// S3 bucket's AWS account. + fn anonymous_client(&self) -> &S3Client { + self.anonymous_client.get_or_init(|| { + // By default, these credentials are anonymous, see https://docs.rs/rusoto_credential/latest/rusoto_credential/struct.AwsCredentials.html#anonymous-example + let credentials = AwsCredentials::default(); + assert!(credentials.is_anonymous(), "AWS credentials not anonymous"); + + S3Client::new_with( + HttpClient::new().unwrap(), + StaticProvider::from(credentials), + self.region.clone(), + ) + }) + } + fn checkpoint_key(index: u32) -> String { format!("checkpoint_{}.json", index) } @@ -88,7 +122,7 @@ impl S3Storage { impl CheckpointSyncer for S3Storage { async fn latest_index(&self) -> Result> { let ret = self - .read_from_bucket(S3Storage::index_key()) + .anonymously_read_from_bucket(S3Storage::index_key()) .await? .map(|data| serde_json::from_slice(&data)) .transpose() @@ -102,13 +136,15 @@ impl CheckpointSyncer for S3Storage { ret } + async fn fetch_checkpoint(&self, index: u32) -> Result> { - self.read_from_bucket(S3Storage::checkpoint_key(index)) + self.anonymously_read_from_bucket(S3Storage::checkpoint_key(index)) .await? .map(|data| serde_json::from_slice(&data)) .transpose() .map_err(Into::into) } + async fn write_checkpoint(&self, signed_checkpoint: SignedCheckpoint) -> Result<()> { let serialized_checkpoint = serde_json::to_string_pretty(&signed_checkpoint)?; self.write_to_bucket( diff --git a/typescript/infra/scripts/verify-validator.ts b/typescript/infra/scripts/verify-validator.ts index 0880409e9..d255c8a3c 100644 --- a/typescript/infra/scripts/verify-validator.ts +++ b/typescript/infra/scripts/verify-validator.ts @@ -3,7 +3,6 @@ import yargs from 'yargs'; import { AllChains, ChainNameToDomainId } from '@hyperlane-xyz/sdk'; -// import { utils } from '@hyperlane-xyz/utils'; import { S3Validator } from '../src/agents/aws/validator'; function getArgs() { @@ -14,10 +13,10 @@ function getArgs() { .describe('address', 'address of the validator to inspect') .demandOption('address') .string('address') - .describe('prospective', 'S3 bucket of the prospective validator') + .describe('prospective', 'S3 bucket URL of the prospective validator') .demandOption('prospective') .string('prospective') - .describe('control', 'S3 bucket of the the known (control) validator') + .describe('control', 'S3 bucket URL of the the known (control) validator') .demandOption('control') .string('control').argv; }