diff --git a/.gitignore b/.gitignore index e89c3e9..8e7ccc5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,9 @@ # will have compiled files and executables debug/ target/ - # These are backup files generated by rustfmt **/*.rs.bk - # MSVC Windows builds of rustc generate these, which store debugging information *.pdb - .DS_Store - .idea/ diff --git a/Cargo.lock b/Cargo.lock index 9ef925c..5e8eae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -957,6 +957,7 @@ dependencies = [ "sp-core 33.0.1", "sp-runtime 37.0.0", "subxt", + "subxt-signer", "thiserror", ] diff --git a/README.md b/README.md index fc11eb7..ef1c46e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,8 @@ Enclave Image successfully created. { "Measurements": { "HashAlgorithm": "Sha384 { ... }", - "PCR0": "77af85a8afd3a38d4f1bcce280d886eac18b60bb8c2365906b88676fe3739458f7854c5fd8eb27d5eb4858269f6686cb", - "PCR1": "4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493", - "PCR2": "d97a4d6458988da4bae2cde519b75bde177fea4454d356921abc339840b28164af049e1bc68ef29fcc9dac9578f3101c" + "PCR0": "d68be77c357668869010a6c56a7d2248e47128eb4aa19f4063bd3edafc075826873661a8dc0ce86321a3eb32274d093a", +... } } ``` @@ -27,7 +26,7 @@ running a Glove enclave on genuine AWS Nitro hardware. > [!NOTE] > The enclave image measurement for the latest build is -> `77af85a8afd3a38d4f1bcce280d886eac18b60bb8c2365906b88676fe3739458f7854c5fd8eb27d5eb4858269f6686cb`. +> `d68be77c357668869010a6c56a7d2248e47128eb4aa19f4063bd3edafc075826873661a8dc0ce86321a3eb32274d093a`. # Running Glove @@ -59,6 +58,123 @@ start the enclave in debug mode and output to the console. > [!WARNING] > Debug mode is not secure and will be reflected in the enclave's remote attestation. Do not enable this in production. +# REST API + +The Glove service exposes a REST API for submitting votes and interacting with it. + +## `GET /info` + +Get information about the Glove service, including the enclave. This can also act as a health check. + +### Request + +None + +### Response + +A JSON object with the following fields: + +#### `proxy_account` + +The Glove proxy account address. Users will need to first assign this account as their +[governance proxy](https://wiki.polkadot.network/docs/learn-proxies#proxy-types) before they can submit votes. + +#### `network_name` + +The substrate-based network the Glove service is connected to. + +#### `node_endpoint` + +The [node endpoint](https://wiki.polkadot.network/docs/maintain-endpoints) URL the service is using to interact with the +network. This is only provided as a convenience for Glove clients, otherwise they can use any node endpoint as long as +it points to the same network. + +#### `attestation_bundle` + +The attestation bundle of the enclave the service is using. This is a hex-encoded string (without the `0x` prefix), +representing the [`AttestationBundle`](common/src/attestation.rs#L43) struct in +[SCALE](https://docs.substrate.io/reference/scale-codec/) encoding. + +The attestation bundle is primarily used in Glove proofs when the enclave submits its mixed votes on-chain. It's +available here for clients to verify the enclave's identity before submitting any votes. + +#### Example + +```json +{ + "proxy_account": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "network_name": "rococo", + "node_endpoint": "wss://rococo-rpc.polkadot.io", + "attestation_bundle": "6408de7737c59c238890533af25896a2c20608d8b380bb01029acb3927..." +} +``` + +## `POST /vote` + +Submit a signed vote request to be included in the Glove mixing process. + +Multiple votes can be submitted for the same poll, but it's up to the discrection of the Glove service to accept them. +If they are accepted they will replace the previous vote for that poll. + +### Request + +A JSON object with the following fields: + +#### `request` + +[SCALE-encoded](https://docs.substrate.io/reference/scale-codec/) [`VoteRequest`](common/src/lib.rs#L36) struct as a +hex string (without the `0x` prefix). + +#### `signature` + +[SCALE-encoded](https://docs.substrate.io/reference/scale-codec/) +[`MultiSignature`](https://docs.rs/sp-runtime/latest/sp_runtime/enum.MultiSignature.html) as a hex string (without the +`0x` prefix). Signed by`VoteRequest.account`, the signature is of the `VoteRequest` in SCALE-encoded bytes, i.e. the +`request` field without the hex-encoding. + +#### Example + +[This example](common/test-resources/vote-request-example.mjs) shows how to create a signed vote request JSON body using +the [Polkadot JS API](https://polkadot.js.org/docs). The request is made by the Bob dev account on the Rococo network +for a vote of aye, on poll 185, using 2.23 ROC at 2x conviction. + +### Response + +If the vote request was successfully received and accepted by the service then an empty response with `200 OK` status +code is returned. This does not mean, however, the vote was mixed and submitted on-chain; just that the Glove service +will do so at the appropriate time. + +If there was something wrong with the vote request then a `400 Bad Request` is returned with a JSON object containing +the error type (`error`) and description (`description`). + +## `POST /remove-vote` + +Submit a signed remove vote request for removing a previously submitted vote. + +### Request + +A JSON object with the following fields: + +#### `request` + +[SCALE-encoded](https://docs.substrate.io/reference/scale-codec/) [`RemoveVoteRequest`](client-interface/src/lib.rs#374) +struct as a hex string (without the `0x` prefix). + +#### `signature` + +[SCALE-encoded](https://docs.substrate.io/reference/scale-codec/) +[`MultiSignature`](https://docs.rs/sp-runtime/latest/sp_runtime/enum.MultiSignature.html) as a hex string (without the +`0x` prefix). Signed by`RemoveVoteRequest.account`, the signature is of the `RemoveVoteRequest` in SCALE-encoded bytes, +i.e. the `request` field without the hex-encoding. + +### Response + +An empty response with `200 OK` status code is returned if the previous vote was successfully removed or if there was +no matching vote. + +If there was something wrong with the request itself then a `400 Bad Request` is returned with a JSON object containing +the error type (`error`) and description (`description`). + # Client CLI There is a CLI client for interacting with the Glove service from the command line. It is built alonside the Glove diff --git a/client-interface/src/lib.rs b/client-interface/src/lib.rs index d878baa..1276286 100644 --- a/client-interface/src/lib.rs +++ b/client-interface/src/lib.rs @@ -5,10 +5,11 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{Context, Result}; -use parity_scale_codec::Decode; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use serde::{Deserialize, Serialize}; use sp_core::crypto::AccountId32; -use sp_runtime::MultiAddress; +use sp_runtime::{MultiAddress, MultiSignature}; +use sp_runtime::traits::Verify; use ss58_registry::{Ss58AddressFormat, Ss58AddressFormatRegistry, Token}; use subxt::Error as SubxtError; use subxt::ext::scale_decode::DecodeAsType; @@ -349,12 +350,27 @@ pub fn account_to_subxt_multi_address(account: AccountId32) -> SubxtMultiAddress #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ServiceInfo { pub proxy_account: AccountId32, - pub network_url: String, + pub network_name: String, + pub node_endpoint: String, #[serde(with = "common::serde_over_hex_scale")] pub attestation_bundle: AttestationBundle } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, MaxEncodedLen)] +pub struct SignedRemoveVoteRequest { + #[serde(with = "common::serde_over_hex_scale")] + pub request: RemoveVoteRequest, + #[serde(with = "common::serde_over_hex_scale")] + pub signature: MultiSignature +} + +impl SignedRemoveVoteRequest { + pub fn verify(&self) -> bool { + self.signature.verify(&*self.request.encode(), &self.request.account) + } +} + +#[derive(Debug, Clone, PartialEq, Encode, Decode, MaxEncodedLen)] pub struct RemoveVoteRequest { pub account: AccountId32, pub poll_index: u32 @@ -380,7 +396,8 @@ mod tests { fn service_info_json() { let service_info = ServiceInfo { proxy_account: dev::alice().public_key().0.into(), - network_url: "wss://polkadot.api.onfinality.io/public-ws".to_string(), + network_name: "polkadot".to_string(), + node_endpoint: "wss://polkadot.api.onfinality.io/public-ws".to_string(), attestation_bundle: AttestationBundle { attested_data: AttestedData { genesis_hash: random::<[u8; 32]>().into(), @@ -397,7 +414,8 @@ mod tests { serde_json::from_str::(&json).unwrap(), json!({ "proxy_account": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - "network_url": "wss://polkadot.api.onfinality.io/public-ws", + "network_name": "polkadot", + "node_endpoint": "wss://polkadot.api.onfinality.io/public-ws", "attestation_bundle": hex::encode(&service_info.attestation_bundle.encode()) }) ); diff --git a/client/src/main.rs b/client/src/main.rs index 2e3ab42..2f175b1 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -14,14 +14,14 @@ use subxt::Error::Runtime; use subxt_signer::sr25519::Keypair; use client::{Error, try_verify_glove_result}; -use client_interface::{account_to_subxt_multi_address, is_glove_member}; +use client_interface::{account_to_subxt_multi_address, is_glove_member, SignedRemoveVoteRequest}; use client_interface::metadata::runtime_types::pallet_proxy::pallet::Error::Duplicate; use client_interface::metadata::runtime_types::pallet_proxy::pallet::Error::NotFound; use client_interface::metadata::runtime_types::polkadot_runtime::{ProxyType, RuntimeError}; use client_interface::RemoveVoteRequest; use client_interface::ServiceInfo; use client_interface::SubstrateNetwork; -use common::{attestation, Conviction, VoteDirection, SignedVoteRequest, VoteRequest}; +use common::{attestation, Conviction, SignedVoteRequest, VoteDirection, VoteRequest}; use common::attestation::{Attestation, EnclaveInfo}; use RuntimeError::Proxy; @@ -38,7 +38,7 @@ async fn main() -> Result { .json::().await?; let network = SubstrateNetwork::connect( - service_info.network_url.clone(), + service_info.node_endpoint.clone(), args.secret_phrase ).await?; @@ -174,13 +174,18 @@ async fn remove_vote( network: &SubstrateNetwork, poll_index: u32 ) -> Result { - let remove_vote_request = RemoveVoteRequest { + let request = RemoveVoteRequest { account: network.account(), poll_index }; + let signature = MultiSignature::Sr25519(network.account_key.sign(&request.encode()).0.into()); + let signed_request = SignedRemoveVoteRequest { request, signature }; + if !signed_request.verify() { + bail!("Something has gone wrong with the signature") + } let response = http_client .post(url_with_path(glove_url, "remove-vote")) - .json(&remove_vote_request) + .json(&signed_request) .send().await .context("Unable to send remove vote request")?; if response.status() == StatusCode::OK { @@ -224,10 +229,10 @@ fn info(service_info: &ServiceInfo) -> Result { Err(attestation_error) => &format!("Error verifying attestation: {}", attestation_error) }; - println!("Glove proxy account: {}", service_info.proxy_account); - println!("Enclave: {}", enclave_info); - println!("Substrate network URL: {}", service_info.network_url); - println!("Genesis hash: {}", hex::encode(ab.attested_data.genesis_hash)); + println!("Glove proxy account: {}", service_info.proxy_account); + println!("Enclave: {}", enclave_info); + println!("Substrate Network: {}", service_info.network_name); + println!("Genesis hash: {}", hex::encode(ab.attested_data.genesis_hash)); Ok(SuccessOutput::None) } diff --git a/common/Cargo.toml b/common/Cargo.toml index f870434..e1113dc 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -23,4 +23,5 @@ hex.workspace = true [dev-dependencies] serde_json.workspace = true +subxt-signer.workspace = true # TODO client-interface and enclave-interface should be merged into common but behind feature flags diff --git a/common/src/attestation.rs b/common/src/attestation.rs index 8214329..7394448 100644 --- a/common/src/attestation.rs +++ b/common/src/attestation.rs @@ -11,6 +11,8 @@ use sp_core::{ed25519, H256, Pair}; use crate::{ExtrinsicLocation, nitro, SignedGloveResult}; +/// Represents a Glove proof of the mixing result done by a secure enclave. The votes on-chain +/// must be compared to the result in the proof to ensure the mixing was done correctly. #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub struct GloveProof { pub signed_result: SignedGloveResult, @@ -40,6 +42,10 @@ impl GloveProof { } } +/// An attestation bundle is a combination of [AttestedData] and [Attestation]. +/// +/// [verify] must be called to ensure the attestation is valid and comes from a genuine secure +/// enclave and to confirm the [AttestedData] matches the attestation. #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub struct AttestationBundle { pub attested_data: AttestedData, @@ -52,7 +58,10 @@ pub struct AttestationBundle { pub const ATTESTATION_BUNDLE_ENCODING_VERSION: u8 = 1; impl AttestationBundle { + /// Verify the attesation and prove the enclave is secure and the attested data came from it. /// + /// Note, this does not prove the attestation is for a enclave running Glove. For that the + /// [EnclaveInfo] that's returned must be checked. pub fn verify(&self) -> Result { match &self.attestation { Attestation::Nitro(nitro_attestation) => { @@ -95,18 +104,26 @@ impl AttestationBundle { } } +/// The attested data that is signed by the enclave. #[derive(Debug, Clone, PartialEq, Encode, Decode, MaxEncodedLen)] pub struct AttestedData { + /// The genesis hash of the chain the enclave is working on. pub genesis_hash: H256, + /// The signing key the enclave is using to sign Glove proofs. pub signing_key: ed25519::Public } +/// Enum of the various attestation types. #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub enum Attestation { + /// AWS Nitro Enclaves attestation Nitro(nitro::Attestation), + /// Marker for a mock enclave. There is no hardware security in a mock enclave and is therefore + /// only suitable for testing. Mock } +/// The information about the enclave that produced the attestation. #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub enum EnclaveInfo { Nitro(nitro::EnclaveInfo) diff --git a/common/src/lib.rs b/common/src/lib.rs index ad3e090..c853f4e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -34,17 +34,28 @@ impl SignedVoteRequest { #[derive(Debug, Clone, PartialEq, Encode, Decode, MaxEncodedLen)] pub struct VoteRequest { + /// The account on whose behalf the Glove proxy will vote for. pub account: AccountId32, + /// The genesis hash of the substrate chain where the poll is taking place. This is necessary + /// to ensure the vote is not replayed on a different chain. pub genesis_hash: H256, + /// The index of the poll/referendum. #[codec(compact)] pub poll_index: u32, - /// Nonce value to prevent replay attacks. Only needs to be unique for the same poll. + /// Nonce value to prevent replay attacks. Needs to be unique for the same poll. pub nonce: u32, + /// `true` for aye, `false` for nay. pub aye: bool, + /// The amount of tokens to vote with. The units are in + /// [Planck](https://wiki.polkadot.network/docs/learn-DOT#the-planck-unit). pub balance: u128, pub conviction: Conviction, } +/// Conviction voting multiplier. +/// +/// See [here](https://wiki.polkadot.network/docs/learn-polkadot-opengov#voluntary-locking-conviction-voting) +/// for more details. #[derive(Debug, Copy, Clone, PartialEq, Encode, Decode, MaxEncodedLen)] pub enum Conviction { None, @@ -69,9 +80,12 @@ impl VoteRequest { } } +/// Signed Glove result from an enclave. The signature can be verified using `signing_key` from +/// the [`attestation::AttestedData`]. #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub struct SignedGloveResult { pub result: GloveResult, + /// Signature of `result` in SCALE endoding. pub signature: ed25519::Signature } @@ -162,6 +176,7 @@ mod tests { use rand::random; use serde_json::{json, Value}; use sp_core::{Pair, sr25519}; + use subxt_signer::sr25519::dev; use Conviction::{Locked3x, Locked6x}; @@ -200,6 +215,27 @@ mod tests { assert_eq!(deserialized_signed_request, signed_request); } + #[test] + fn signed_vote_request_json_using_polkadot_js() { + // Produced from test-resources/vote-request-example.mjs + let json = r#" +{ + "request": "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a486408de7737c59c238890533af25896a2c20608d8b380bb01029acb392781063ee502f5c1912301009c5b3607020000000000000000000002", + "signature": "01ea13f59165bc295d1e99629622dc0c13a1c7163017359178606f0e36d1d2c246c021190cf0508b0d60d292a00ba06ed396d5559fe7334c78c95bf289e84ebc81" +} +"#; + + let signed_request = serde_json::from_str::(json).unwrap(); + assert!(signed_request.verify()); + let request = signed_request.request; + assert_eq!(request.account, dev::bob().public_key().0.into()); + assert_eq!(request.poll_index, 185); + assert_eq!(request.genesis_hash, H256::from_str("6408de7737c59c238890533af25896a2c20608d8b380bb01029acb392781063e").unwrap()); + assert_eq!(request.balance, 2230000000000); + assert_eq!(request.aye, true); + assert_eq!(request.conviction, Conviction::Locked2x); + } + #[test] fn different_signer_to_vote_request_account() { let (pair1, _) = sr25519::Pair::generate(); diff --git a/common/src/nitro.rs b/common/src/nitro.rs index d5f58f9..abd304e 100644 --- a/common/src/nitro.rs +++ b/common/src/nitro.rs @@ -14,10 +14,15 @@ static ROOT_CA_BYTES: &[u8] = include_bytes!("../../assets/aws-nitro-root.pem"); #[derive(Debug, Clone, PartialEq, Encode, Decode)] pub struct EnclaveInfo { + /// The enclave image measurement (`PCR0`). This value must be checked against known + /// measurements of Glove enclaves. pub image_measurement: Vec, - // TODO Image signer } +/// Attestation document from AWS Nitro Enclaves. +/// +/// The enclave itself by either be running in secure mode or debug mode. This can be determined by +/// calling [crate::attestation::AttestationBundle::verify]. #[derive(Debug, Clone)] pub struct Attestation(CoseSign1); diff --git a/common/test-resources/vote-request-example.mjs b/common/test-resources/vote-request-example.mjs new file mode 100644 index 0000000..389a210 --- /dev/null +++ b/common/test-resources/vote-request-example.mjs @@ -0,0 +1,61 @@ +// Requires node version 21.7.3 + +import {ApiPromise, Keyring, WsProvider} from '@polkadot/api'; +import {randomBytes} from 'crypto'; + +async function main () { + const api = await ApiPromise.create({ + provider: new WsProvider('wss://rococo-rpc.polkadot.io'), + // Register the Glove types + types: { + VoteRequest: { + "account": "AccountId32", + "genesis_hash": "H256", + "poll_index": "Compact", + "nonce": "u32", + "aye": "bool", + "balance": "u128", + "conviction": "Conviction" + }, + Conviction: { + "_enum": { + "None": null, + "Locked1x": null, + "Locked2x": null, + "Locked3x": null, + "Locked4x": null, + "Locked5x": null, + "Locked6x": null + } + } + } + }); + + const keyring = new Keyring({ type: 'sr25519' }); + const bob = keyring.addFromUri('//Bob', { name: 'Bob' }); + + // Generate a random nonce + const nonce = randomBytes(4).readUint32BE(0); + + const voteRequest = api.createType('VoteRequest', { + account: bob.address, + genesis_hash: api.genesisHash, + poll_index: 185, + nonce: nonce, + aye: true, + // Use the decimal information from the chain to convert to Planck units + balance: 2.23 * Math.pow(10, api.registry.chainDecimals[0]), + conviction: 'Locked2x' + }); + + const signature = bob.sign(voteRequest.toU8a(), { withType: true }); + + const signedVoteRequest = { + request: Buffer.from(voteRequest.toU8a()).toString('hex'), + signature: Buffer.from(signature).toString('hex') + }; + + console.log(JSON.stringify(signedVoteRequest, null, 2)); +} + +main().catch(console.error).finally(() => process.exit()); diff --git a/service/src/lib.rs b/service/src/lib.rs index 7d74e7c..82a0ca2 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -12,6 +12,7 @@ use common::SignedVoteRequest; pub mod enclave; pub mod subscan; +pub mod mixing; #[derive(Default)] pub struct GloveState { diff --git a/service/src/main.rs b/service/src/main.rs index 3395d64..5a66068 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -1,4 +1,3 @@ -use io::Error as IoError; use std::io; use std::sync::Arc; use std::time::Duration; @@ -10,6 +9,7 @@ use axum::Router; use axum::routing::{get, post}; use cfg_if::cfg_if; use clap::{Parser, ValueEnum}; +use serde::Serialize; use sp_runtime::AccountId32; use subxt::Error as SubxtError; use subxt_signer::sr25519::Keypair; @@ -17,12 +17,12 @@ use tokio::net::TcpListener; use tokio::spawn; use tokio::time::sleep; use tower_http::trace::TraceLayer; -use tracing::{debug, info}; +use tracing::{debug, error, info, trace}; use tracing::log::warn; use tracing_subscriber::filter::{EnvFilter, LevelFilter}; use attestation::Error::InsecureMode; -use client_interface::{account, is_glove_member, ProxyError, ServiceInfo, SubstrateNetwork}; +use client_interface::{account, is_glove_member, ProxyError, ServiceInfo, SignedRemoveVoteRequest, SubstrateNetwork}; use client_interface::account_to_subxt_multi_address; use client_interface::BatchError; use client_interface::metadata::runtime_types::frame_system::pallet::Call as SystemCall; @@ -35,17 +35,11 @@ use client_interface::metadata::runtime_types::pallet_proxy::pallet::Error::NotP use client_interface::metadata::runtime_types::polkadot_runtime::RuntimeCall; use client_interface::metadata::runtime_types::polkadot_runtime::RuntimeError; use client_interface::metadata::runtime_types::polkadot_runtime::RuntimeError::Proxy; -use client_interface::RemoveVoteRequest; use common::{AssignedBalance, attestation, BASE_AYE, BASE_NAY, Conviction, GloveResult, SignedGloveResult, SignedVoteRequest, VoteDirection}; -use common::attestation::{AttestationBundle, AttestationBundleLocation, GloveProof, GloveProofLite}; -use enclave_interface::{EnclaveRequest, EnclaveResponse}; +use common::attestation::{AttestationBundle, AttestationBundleLocation, GloveProofLite}; use RuntimeError::ConvictionVoting; -use service::{GloveState, Poll, subscan}; +use service::{GloveState, mixing, Poll, subscan}; use service::enclave::EnclaveHandle; -use ServiceError::{NotMember, PollNotOngoing}; -use ServiceError::ChainMismatch; -use ServiceError::InsufficientBalance; -use ServiceError::InvalidSignature; #[derive(Parser, Debug)] #[command(version, about = "Glove proxy service")] @@ -104,7 +98,6 @@ enum EnclaveMode { // TODO Update client to make it easy to verify on-chain vote // TODO No more votes after on-chain votes - #[tokio::main] async fn main() -> anyhow::Result<()> { let filter = EnvFilter::try_new("subxt_core::events=info,hyper_util=info,reqwest::connect=info")? @@ -119,7 +112,7 @@ async fn main() -> anyhow::Result<()> { let enclave_handle = initialize_enclave(args.enclave_mode).await?; let network = SubstrateNetwork::connect(args.node_endpoint, args.proxy_secret_phrase).await?; - info!("Connected: {:?}", network); + debug!("Connected: {:?}", network); let attestation_bundle = enclave_handle.send_receive::( &network.api.genesis_hash() @@ -147,6 +140,7 @@ async fn main() -> anyhow::Result<()> { .layer(TraceLayer::new_for_http()) .with_state(glove_context); let listener = TcpListener::bind(args.address).await?; + info!("Listening for requests..."); axum::serve(listener, router).await?; Ok(()) @@ -156,7 +150,7 @@ async fn main() -> anyhow::Result<()> { fn start_background_checker(context: Arc) { spawn(async move { loop { - debug!("Checking for Glove violators..."); + trace!("Checking for Glove violators..."); if let Err(error) = context.remove_glove_violators().await { warn!("Error when checking for Glove violators: {:?}", error) } @@ -172,7 +166,7 @@ async fn initialize_enclave(enclave_mode: EnclaveMode) -> io::Result io::Result io::Result>) -> Json { Json(ServiceInfo { proxy_account: context.network.account(), - network_url: context.network.url.clone(), + network_name: context.network.network_name.clone(), + node_endpoint: context.network.url.clone(), attestation_bundle: context.attestation_bundle.clone() }) } @@ -214,28 +209,28 @@ async fn info(context: State>) -> Json { async fn vote( State(context): State>, Json(signed_request): Json -) -> Result<(), ServiceError> { +) -> Result<(), VoteError> { let network = &context.network; let request = &signed_request.request; if !signed_request.verify() { - return Err(InvalidSignature); + return Err(BadVoteRequestError::InvalidSignature.into()); } if request.genesis_hash != network.api.genesis_hash() { - return Err(ChainMismatch); + return Err(BadVoteRequestError::ChainMismatch.into()); } if !is_glove_member(network, request.account.clone(), network.account()).await? { - return Err(NotMember); + return Err(BadVoteRequestError::NotMember.into()); } if network.get_ongoing_poll(request.poll_index).await?.is_none() { - return Err(PollNotOngoing); + return Err(BadVoteRequestError::PollNotOngoing.into()); } // In a normal poll with multiple votes on both sides, the on-chain vote balance can be // significantly less than the vote request balance. A malicious actor could use this to scew // the poll by passing a balance value much higher than they have, knowing there's a good chance // it won't be fully utilised. if network.account_balance(request.account.clone()).await? < request.balance { - return Err(InsufficientBalance); + return Err(BadVoteRequestError::InsufficientBalance.into()); } let poll = context.state.get_poll(request.poll_index); let initiate_mix = poll.add_vote_request(signed_request).await; @@ -247,14 +242,18 @@ async fn vote( async fn remove_vote( State(context): State>, - Json(payload): Json -) -> Result<(), ServiceError> { + Json(signed_request): Json +) -> Result<(), RemoveVoteError> { let network = &context.network; - let account = &payload.account; + let request = &signed_request.request; + let account = &request.account; + if !signed_request.verify() { + return Err(BadRemoveVoteRequestError::InvalidSignature.into()); + } if !is_glove_member(network, account.clone(), network.account()).await? { - return Err(NotMember); + return Err(BadRemoveVoteRequestError::NotMember.into()); } - let Some(poll) = context.state.get_optional_poll(payload.poll_index) else { + let Some(poll) = context.state.get_optional_poll(request.poll_index) else { // Removing a non-existent vote request is a no-op return Ok(()); }; @@ -262,21 +261,19 @@ async fn remove_vote( // Another task has already started mixing the votes return Ok(()); }; - - let remove_result = proxy_remove_vote(network, payload.account, payload.poll_index).await; + let remove_result = proxy_remove_vote(network, account.clone(), request.poll_index).await; match remove_result { // Unlikely since we've just checked above, but just in case Err(ProxyError::Module(_, ConvictionVoting(NotVoter))) => return Ok(()), - Err(ProxyError::Batch(BatchError::Module(_, Proxy(NotProxy)))) => return Err(NotMember), + Err(ProxyError::Batch(BatchError::Module(_, Proxy(NotProxy)))) => + return Err(BadRemoveVoteRequestError::NotMember.into()), Err(error) => return Err(error.into()), Ok(_) => {} } - if initiate_mix { // TODO Only do the mixing if the votes were previously submitted on-chain schedule_vote_mixing(context, poll); } - Ok(()) } @@ -305,14 +302,18 @@ async fn mix_votes(context: &GloveContext, poll: &Poll) { } } -async fn try_mix_votes(context: &GloveContext, poll: &Poll) -> Result { +async fn try_mix_votes(context: &GloveContext, poll: &Poll) -> Result { info!("Mixing votes for poll {}", poll.index); let Some(poll_requests) = poll.begin_mix().await else { // Another task has already started mixing the votes return Ok(true); }; - let signed_glove_result = mix_votes_in_enclave(&context, poll_requests.clone()).await?; + let signed_glove_result = mixing::mix_votes_in_enclave( + &context.enclave_handle, + &context.attestation_bundle, + poll_requests.clone() + ).await?; let result = submit_glove_result_on_chain(&context, &poll_requests, signed_glove_result).await; if result.is_ok() { @@ -352,35 +353,6 @@ async fn try_mix_votes(context: &GloveContext, poll: &Poll) -> Result -) -> Result { - let request = EnclaveRequest::MixVotes(vote_requests); - let response = context.enclave_handle.send_receive::(&request).await?; - match response { - EnclaveResponse::GloveResult(signed_result) => { - let result = &signed_result.result; - debug!("Glove result from enclave, poll: {}, direction: {:?}, signature: {:?}", - result.poll_index, result.direction, signed_result.signature); - for assigned_balance in &result.assigned_balances { - debug!(" {:?}", assigned_balance); - } - // Double-check things all line up before committing on-chain - match GloveProof::verify_components(&signed_result, &context.attestation_bundle) { - Ok(_) => debug!("Glove proof verified"), - Err(InsecureMode) => warn!("Glove proof from insecure enclave"), - Err(error) => return Err(error.into()) - } - Ok(signed_result) - } - EnclaveResponse::Error(enclave_error) => { - warn!("Mixing error from enclave: {:?}", enclave_error); - Err(enclave_error.into()) - }, - } -} - async fn submit_glove_result_on_chain( context: &GloveContext, signed_requests: &Vec, @@ -504,6 +476,7 @@ impl GloveContext { for poll in self.state.get_polls() { // Use this opportunity to do some garbage collection and remove any expired polls if self.network.get_ongoing_poll(poll.index).await?.is_none() { + debug!("Removing poll {} as it is no longer ongoing", poll.index); self.state.remove_poll(poll.index); continue; } @@ -547,44 +520,131 @@ impl GloveContext { } #[derive(thiserror::Error, Debug)] -pub enum MixingError { - #[error("IO error: {0}")] - Io(#[from] IoError), - #[error("Enclave error: {0}")] - Enclave(#[from] enclave_interface::Error), - #[error("Enclave attestation error: {0}")] - Attestation(#[from] attestation::Error) +enum InternalError { + #[error("Subxt error: {0}")] + Subxt(#[from] SubxtError), + #[error("Proxy error: {0}")] + Proxy(#[from] ProxyError) } #[derive(thiserror::Error, Debug)] -enum ServiceError { +enum BadVoteRequestError { #[error("Signature on signed vote request is invalid")] InvalidSignature, #[error("Vote request is for a different chain")] ChainMismatch, - #[error("Client is not a member of the Glove proxy")] + #[error("Glove proxy is not assigned as a Governance proxy to the account")] NotMember, #[error("Poll is not ongoing or does not exist")] PollNotOngoing, #[error("Insufficient account balance for vote")] - InsufficientBalance, - #[error("Proxy error: {0}")] - Proxy(#[from] ProxyError), - #[error("Internal Subxt error: {0}")] - Subxt(#[from] SubxtError), + InsufficientBalance +} + +#[derive(thiserror::Error, Debug)] +enum VoteError { + #[error("Bad request: {0}")] + BadRequest(#[from] BadVoteRequestError), + #[error("Internal error: {0}")] + Internal(#[from] InternalError) +} + +impl From for VoteError { + fn from(error: SubxtError) -> Self { + VoteError::Internal(error.into()) + } +} + +impl From for VoteError { + fn from(error: ProxyError) -> Self { + VoteError::Internal(error.into()) + } +} + +#[derive(Serialize)] +struct BadRequestResponse { + error: String, + description: String +} + +impl IntoResponse for VoteError { + fn into_response(self) -> Response { + match self { + VoteError::BadRequest(error) => { + let error_variant = match error { + BadVoteRequestError::InvalidSignature => "InvalidSignature", + BadVoteRequestError::ChainMismatch => "ChainMismatch", + BadVoteRequestError::NotMember => "NotMember", + BadVoteRequestError::PollNotOngoing => "PollNotOngoing", + BadVoteRequestError::InsufficientBalance => "InsufficientBalance", + }.to_string(); + ( + StatusCode::BAD_REQUEST, + Json(BadRequestResponse { error: error_variant, description: error.to_string() }) + ).into_response() + }, + VoteError::Internal(error) => { + warn!("Error with vote request: {:?}", error); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string() + ).into_response() + } + } + } +} + +#[derive(thiserror::Error, Debug)] +enum BadRemoveVoteRequestError { + #[error("Signature on signed vote request is invalid")] + InvalidSignature, + #[error("Glove proxy is not assigned as a Governance proxy to the account")] + NotMember } -impl IntoResponse for ServiceError { +#[derive(thiserror::Error, Debug)] +enum RemoveVoteError { + #[error("Bad request: {0}")] + BadRequest(#[from] BadRemoveVoteRequestError), + #[error("Internal error: {0}")] + Internal(#[from] InternalError) +} + +impl From for RemoveVoteError { + fn from(error: SubxtError) -> Self { + RemoveVoteError::Internal(error.into()) + } +} + +impl From for RemoveVoteError { + fn from(error: ProxyError) -> Self { + RemoveVoteError::Internal(error.into()) + } +} + +impl IntoResponse for RemoveVoteError { fn into_response(self) -> Response { match self { - ChainMismatch => (StatusCode::BAD_REQUEST, self.to_string()), - NotMember => (StatusCode::BAD_REQUEST, self.to_string()), - PollNotOngoing => (StatusCode::BAD_REQUEST, self.to_string()), - InsufficientBalance => (StatusCode::BAD_REQUEST, self.to_string()), - _ => { - warn!("{:?}", self); - (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()) + RemoveVoteError::BadRequest(error) => { + let error_variant = match error { + BadRemoveVoteRequestError::InvalidSignature => "InvalidSignature", + BadRemoveVoteRequestError::NotMember => "NotMember", + }.to_string(); + ( + StatusCode::BAD_REQUEST, + Json(BadRequestResponse { + error: error_variant, + description: error.to_string() + }) + ).into_response() + }, + RemoveVoteError::Internal(error) => { + warn!("Error with remove-vote request: {:?}", error); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string() + ).into_response() } - }.into_response() + } } } diff --git a/service/src/mixing.rs b/service/src/mixing.rs new file mode 100644 index 0000000..a311142 --- /dev/null +++ b/service/src/mixing.rs @@ -0,0 +1,51 @@ +use std::io; + +use tracing::{debug}; +use tracing::log::warn; + +use common::{attestation, SignedGloveResult, SignedVoteRequest}; +use common::attestation::{AttestationBundle, GloveProof}; +use common::attestation::Error::InsecureMode; +use enclave_interface::{EnclaveRequest, EnclaveResponse}; + +use crate::enclave::EnclaveHandle; + +pub async fn mix_votes_in_enclave( + enclave_handle: &EnclaveHandle, + attestation_bundle: &AttestationBundle, + vote_requests: Vec +) -> Result { + let request = EnclaveRequest::MixVotes(vote_requests); + let response = enclave_handle.send_receive::(&request).await?; + match response { + EnclaveResponse::GloveResult(signed_result) => { + let result = &signed_result.result; + debug!("Glove result from enclave, poll: {}, direction: {:?}, signature: {:?}", + result.poll_index, result.direction, signed_result.signature); + for assigned_balance in &result.assigned_balances { + debug!(" {:?}", assigned_balance); + } + // Double-check things all line up before committing on-chain + match GloveProof::verify_components(&signed_result, &attestation_bundle) { + Ok(_) => debug!("Glove proof verified"), + Err(InsecureMode) => warn!("Glove proof from insecure enclave"), + Err(error) => return Err(error.into()) + } + Ok(signed_result) + } + EnclaveResponse::Error(enclave_error) => { + warn!("Mixing error from enclave: {:?}", enclave_error); + Err(enclave_error.into()) + }, + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("Enclave error: {0}")] + Enclave(#[from] enclave_interface::Error), + #[error("Enclave attestation error: {0}")] + Attestation(#[from] attestation::Error) +} diff --git a/service/src/subscan.rs b/service/src/subscan.rs index 677778b..386c73a 100644 --- a/service/src/subscan.rs +++ b/service/src/subscan.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use serde_with::DisplayFromStr; use serde_with::serde_as; use sp_runtime::AccountId32; -use tracing::debug; use client_interface::SubstrateNetwork; use common::ExtrinsicLocation; @@ -24,7 +23,6 @@ pub async fn get_votes( }; loop { - debug!("Fetching votes: {:?}", &request); let response = http_client .post(&url) .json(&request) @@ -33,9 +31,6 @@ pub async fn get_votes( let Some(mut votes) = response.data.list else { break; }; - for vote in &votes { - debug!(" {:?}", vote); - } all_votes.append(&mut votes); request.page += 1; }