Skip to content

Commit

Permalink
Updated README with Glove service REST API doc (#14)
Browse files Browse the repository at this point in the history
This includes a Polkadot JS sample which creates a signed vote request
payload for the `/vote` endpoint. This endpoint was also updated to
return a JSON object if the request was bad with error information.

`/remove-vote` had to be fixed to be a signed request, otherwise anyone
can remove a vote.

Added `network_name` field to `/info` output, and `network_url` was
renamed to `node_endpoint`

Also added some documentation to the public Rust structs.
  • Loading branch information
shamsasari authored Jul 15, 2024
1 parent b317cea commit 32340c8
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 117 deletions.
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 120 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ Enclave Image successfully created.
{
"Measurements": {
"HashAlgorithm": "Sha384 { ... }",
"PCR0": "77af85a8afd3a38d4f1bcce280d886eac18b60bb8c2365906b88676fe3739458f7854c5fd8eb27d5eb4858269f6686cb",
"PCR1": "4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493",
"PCR2": "d97a4d6458988da4bae2cde519b75bde177fea4454d356921abc339840b28164af049e1bc68ef29fcc9dac9578f3101c"
"PCR0": "d68be77c357668869010a6c56a7d2248e47128eb4aa19f4063bd3edafc075826873661a8dc0ce86321a3eb32274d093a",
...
}
}
```
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
30 changes: 24 additions & 6 deletions client-interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -397,7 +414,8 @@ mod tests {
serde_json::from_str::<Value>(&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())
})
);
Expand Down
23 changes: 14 additions & 9 deletions client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -38,7 +38,7 @@ async fn main() -> Result<SuccessOutput> {
.json::<ServiceInfo>().await?;

let network = SubstrateNetwork::connect(
service_info.network_url.clone(),
service_info.node_endpoint.clone(),
args.secret_phrase
).await?;

Expand Down Expand Up @@ -174,13 +174,18 @@ async fn remove_vote(
network: &SubstrateNetwork,
poll_index: u32
) -> Result<SuccessOutput> {
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 {
Expand Down Expand Up @@ -224,10 +229,10 @@ fn info(service_info: &ServiceInfo) -> Result<SuccessOutput> {
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)
}
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions common/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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<EnclaveInfo, Error> {
match &self.attestation {
Attestation::Nitro(nitro_attestation) => {
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 32340c8

Please sign in to comment.