From fc054b6e5c24e7171a3a6cc64fc066a9332d39d2 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 15 Jul 2024 11:52:34 +0100 Subject: [PATCH] Updated README with Glove servive REST API doc This includes a Polkadot JS sample which creates a valid payload for the `/vote` endpoint. This endpoint was also updated to return a JSON object if the request was bad with the error information. Also added some documentation to the public Rust structs --- .gitignore | 4 - Cargo.lock | 1 + README.md | 96 ++++++++- client-interface/src/lib.rs | 9 +- client/src/main.rs | 10 +- common/Cargo.toml | 1 + common/src/attestation.rs | 17 ++ common/src/lib.rs | 38 +++- common/src/nitro.rs | 7 +- .../test-resources/vote-request-example.mjs | 61 ++++++ service/src/lib.rs | 1 + service/src/main.rs | 197 +++++++++++------- service/src/mixing.rs | 51 +++++ 13 files changed, 400 insertions(+), 93 deletions(-) create mode 100644 common/test-resources/vote-request-example.mjs create mode 100644 service/src/mixing.rs 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..05161a9 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,95 @@ 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`). + # 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..2522ab0 100644 --- a/client-interface/src/lib.rs +++ b/client-interface/src/lib.rs @@ -349,7 +349,8 @@ 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 } @@ -380,7 +381,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 +399,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..77d257d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -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?; @@ -224,10 +224,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..61bb776 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; @@ -37,15 +37,10 @@ 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 +99,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")? @@ -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; @@ -245,14 +240,15 @@ async fn vote( Ok(()) } +// TODO The request needs to be signed, otherwise anyone can remove anyone else's vote async fn remove_vote( State(context): State>, Json(payload): Json -) -> Result<(), ServiceError> { +) -> Result<(), RemoveVoteError> { let network = &context.network; let account = &payload.account; if !is_glove_member(network, account.clone(), network.account()).await? { - return Err(NotMember); + return Err(RemoveVoteError::NotMember); } let Some(poll) = context.state.get_optional_poll(payload.poll_index) else { // Removing a non-existent vote request is a no-op @@ -267,7 +263,8 @@ async fn remove_vote( 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(RemoveVoteError::NotMember), Err(error) => return Err(error.into()), 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, @@ -547,44 +519,119 @@ 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 ServiceError { +impl IntoResponse for VoteError { 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()) + 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() } - }.into_response() + } + } +} + +#[derive(thiserror::Error, Debug)] +enum RemoveVoteError { + #[error("Glove proxy is not assigned as a Governance proxy to the account")] + NotMember, + #[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 { + RemoveVoteError::NotMember => { + ( + StatusCode::BAD_REQUEST, + Json(BadRequestResponse { + error: "NotMember".to_string(), + description: self.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() + } + } } } 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) +}