feat: decoupling quartz from applications, creating transfers app (#64)

Co-authored-by: hu55a1n1 <sufialhussaini@gmail.com>
Co-authored-by: Ethan Buchman <ethan@coinculture.info>
Co-authored-by: David Kajpust <kajpustd@gmail.com>
This commit is contained in:
Daniel Gushchyan 2024-06-26 12:31:01 -07:00 committed by GitHub
parent fcad39e5d0
commit 47544c66ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 7930 additions and 28 deletions

61
Cargo.lock generated
View file

@ -477,6 +477,7 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d184abb7b0039cc64f282dfa5b34165e4c5a7410ab46804636d53f4d09aee44" checksum = "5d184abb7b0039cc64f282dfa5b34165e4c5a7410ab46804636d53f4d09aee44"
dependencies = [ dependencies = [
"bip32",
"cosmos-sdk-proto", "cosmos-sdk-proto",
"ecdsa", "ecdsa",
"eyre", "eyre",
@ -652,6 +653,25 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "cw-multi-test"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d818f5323c80ed4890db7f89d65eda3f0261fe21878e628c27ea2d8de4b7ba4"
dependencies = [
"anyhow",
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"derivative",
"itertools 0.11.0",
"prost",
"schemars",
"serde",
"sha2 0.10.8",
"thiserror",
]
[[package]] [[package]]
name = "cw-proof" name = "cw-proof"
version = "0.1.0" version = "0.1.0"
@ -1020,6 +1040,36 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "enclave"
version = "0.1.0"
dependencies = [
"clap",
"color-eyre",
"cosmrs",
"cosmwasm-std",
"cw-multi-test",
"cw-tee-mtcs",
"cycles-sync",
"ecies",
"hex",
"k256",
"mtcs",
"prost",
"quartz-cw",
"quartz-enclave",
"quartz-proto",
"schemars",
"serde",
"serde_json",
"tendermint 0.36.0",
"tendermint-light-client",
"thiserror",
"tokio",
"tonic",
"tonic-build",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.34" version = "0.8.34"
@ -1568,6 +1618,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -2312,7 +2371,6 @@ dependencies = [
"hex", "hex",
"k256", "k256",
"mtcs", "mtcs",
"prost",
"quartz-cw", "quartz-cw",
"quartz-proto", "quartz-proto",
"quartz-relayer", "quartz-relayer",
@ -2325,7 +2383,6 @@ dependencies = [
"tm-stateless-verifier", "tm-stateless-verifier",
"tokio", "tokio",
"tonic", "tonic",
"tonic-build",
] ]
[[package]] [[package]]

View file

@ -1,12 +1,12 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [ "apps/mtcs/enclave",
"core/light-client-proofs/*", "core/light-client-proofs/*",
"core/quartz", "core/quartz",
"cosmwasm/packages/*", "cosmwasm/packages/*",
"utils/*", "utils/*",
] ]
exclude = ["apps/mtcs/contracts/cw-tee-mtcs"] exclude = ["apps/mtcs/contracts/cw-tee-mtcs", "apps/mtcs/enclave", "apps/transfers", "apps/transfers/enclave"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"

View file

@ -0,0 +1,52 @@
[package]
name = "enclave"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[dependencies]
# external
hex = { version = "0.4.3", default-features = false }
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
schemars = "0.8.15"
serde = { version = "1.0.189", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.49" }
tonic = { version = "0.11.0"}
tonic-build = "0.11.0"
cosmrs = { version = "0.16.0"}
cosmwasm-std = { version = "1.5.2", default-features = false }
serde_json = { version = "1.0.94", default-features = false }
ecies = { version = "0.2.3", default-features = false, features = ["pure"] }
clap = { version = "4.1.8", default-features = false, features = ["derive", "std"] }
tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] }
tendermint = { version = "=0.36.0", default-features = false }
tendermint-light-client = { version = "=0.36.0", default-features = false, features = ["rust-crypto"] }
color-eyre = { version = "0.6.2", default-features = false }
prost = { version = "0.12.3", default-features = false }
# local
cw-tee-mtcs.workspace = true
cycles-sync.workspace = true
mtcs.workspace = true
# quartz
quartz-cw = { path = "../../../cosmwasm/packages/quartz-cw" }
quartz-proto = { path = "../../../core/quartz-proto" }
quartz-enclave = { path = "../../../core/quartz"}
[dev-dependencies]
cw-multi-test = "0.17.0"
serde_json = "1.0.113"
[build-dependencies]
tonic-build.workspace = true

View file

@ -0,0 +1 @@
## MTCS Server

View file

@ -2,7 +2,7 @@ use std::net::SocketAddr;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::{eyre, Result}; use color_eyre::eyre::{eyre, Result};
use tendermint::Hash; use cosmrs::tendermint::Hash;
use tendermint_light_client::types::{Height, TrustThreshold}; use tendermint_light_client::types::{Height, TrustThreshold};
fn parse_trust_threshold(s: &str) -> Result<TrustThreshold> { fn parse_trust_threshold(s: &str) -> Result<TrustThreshold> {

View file

@ -13,11 +13,9 @@
unused_qualifications unused_qualifications
)] )]
mod attestor;
mod cli; mod cli;
mod mtcs_server; mod mtcs_server;
mod proto; mod proto;
mod server;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -25,17 +23,16 @@ use std::{
}; };
use clap::Parser; use clap::Parser;
use cli::Cli;
use mtcs_server::MtcsService;
use proto::clearing_server::ClearingServer as MtcsServer;
use quartz_cw::state::{Config, LightClientOpts}; use quartz_cw::state::{Config, LightClientOpts};
use quartz_proto::quartz::core_server::CoreServer; use quartz_enclave::{
use tonic::transport::Server;
use crate::{
attestor::{Attestor, EpidAttestor}, attestor::{Attestor, EpidAttestor},
cli::Cli,
mtcs_server::MtcsService,
proto::clearing_server::ClearingServer as MtcsServer,
server::CoreService, server::CoreService,
}; };
use quartz_proto::quartz::core_server::CoreServer;
use tonic::transport::Server;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {

View file

@ -5,9 +5,10 @@ use std::{
use cosmrs::{tendermint::account::Id as TmAccountId, AccountId}; use cosmrs::{tendermint::account::Id as TmAccountId, AccountId};
use cosmwasm_std::HexBinary; use cosmwasm_std::HexBinary;
//TODO: get rid of this
use cw_tee_mtcs::{ use cw_tee_mtcs::{
msg::execute::SubmitSetoffsMsg, msg::execute::SubmitSetoffsMsg,
state::{RawCipherText, RawHash, SettleOff, Transfer}, state::{RawHash, SettleOff, Transfer},
}; };
use cycles_sync::types::RawObligation; use cycles_sync::types::RawObligation;
use ecies::{decrypt, encrypt}; use ecies::{decrypt, encrypt};
@ -16,13 +17,14 @@ use mtcs::{
algo::mcmf::primal_dual::PrimalDual, impls::complex_id::ComplexIdMtcs, algo::mcmf::primal_dual::PrimalDual, impls::complex_id::ComplexIdMtcs,
obligation::SimpleObligation, prelude::DefaultMtcs, setoff::SimpleSetoff, Mtcs, obligation::SimpleObligation, prelude::DefaultMtcs, setoff::SimpleSetoff, Mtcs,
}; };
use quartz_cw::msg::execute::attested::RawAttested;
use quartz_enclave::attestor::Attestor;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tonic::{Request, Response, Result as TonicResult, Status}; use tonic::{Request, Response, Result as TonicResult, Status};
use crate::{ use crate::proto::{clearing_server::Clearing, RunClearingRequest, RunClearingResponse};
attestor::Attestor,
proto::{clearing_server::Clearing, RunClearingRequest, RunClearingResponse}, pub type RawCipherText = HexBinary;
};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MtcsService<A> { pub struct MtcsService<A> {
@ -60,6 +62,23 @@ where
&self, &self,
request: Request<RunClearingRequest>, request: Request<RunClearingRequest>,
) -> TonicResult<Response<RunClearingResponse>> { ) -> TonicResult<Response<RunClearingResponse>> {
// Pass in JSON of Requests vector and the STATE
// Serialize into Requests enum
// Loop through, decrypt the ciphertexts
// Read the state blob from chain
// Decrypt and deserialize
// Loop through requests and apply onto state
// Encrypt state
// Create withdraw requests
// Send to chain
let message: RunClearingMessage = { let message: RunClearingMessage = {
let message = request.into_inner().message; let message = request.into_inner().message;
serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))? serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))?
@ -93,12 +112,12 @@ where
let msg = SubmitSetoffsMsg { setoffs_enc }; let msg = SubmitSetoffsMsg { setoffs_enc };
let quote = self let attestation = self
.attestor .attestor
.quote(msg.clone()) .quote(msg.clone())
.map_err(|e| Status::internal(e.to_string()))?; .map_err(|e| Status::internal(e.to_string()))?;
let attested_msg = AttestedMsg { msg, quote }; let attested_msg = RawAttested { msg, attestation };
let message = serde_json::to_string(&attested_msg).unwrap(); let message = serde_json::to_string(&attested_msg).unwrap();
Ok(Response::new(RunClearingResponse { message })) Ok(Response::new(RunClearingResponse { message }))
} }

View file

@ -0,0 +1,5 @@
[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"
schema = "run schema"

1102
apps/transfers/contracts/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
[package]
name = "transfers_contracts"
version = "0.1.0"
edition = "2021"
exclude = ["contract.wasm", "hash.txt"]
[[bin]]
name = "schema"
path = "bin/schema.rs"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
backtraces = ["cosmwasm-std/backtraces"]
[dependencies]
# external
sha2 = "0.10.8"
serde_json = "1.0.117"
thiserror = { version = "1.0.49" }
# cosmwasm
cosmwasm-schema = "1.5.0"
cosmwasm-std = { version = "1.5.0", features = ["cosmwasm_1_3"] }
cw-storage-plus = "1.1.0"
cw-utils = "1.0.3"
cw2 = "1.1.1"
cw20-base = { version = "1.1.1", features = ["library"] }
# quartz
quartz-cw = { path = "../../../cosmwasm/packages/quartz-cw" }

View file

@ -0,0 +1,9 @@
use cosmwasm_schema::write_api;
use transfers_contracts::msg::{ExecuteMsg, InstantiateMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ExecuteMsg,
}
}

View file

@ -0,0 +1,321 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"type": "string",
"enum": [
"deposit",
"withdraw"
]
},
{
"type": "object",
"required": [
"quartz"
],
"properties": {
"quartz": {
"$ref": "#/definitions/RawExecute_for_RawEpidAttestation"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"transfer_request"
],
"properties": {
"transfer_request": {
"$ref": "#/definitions/TransferRequestMsg"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"clear_text_transfer_request"
],
"properties": {
"clear_text_transfer_request": {
"$ref": "#/definitions/ClearTextTransferRequestMsg"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"update"
],
"properties": {
"update": {
"$ref": "#/definitions/RawAttested_for_RawUpdateMsg_and_RawEpidAttestation"
}
},
"additionalProperties": false
}
],
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"ClearTextTransferRequestMsg": {
"type": "object",
"required": [
"amount",
"receiver",
"sender"
],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"receiver": {
"$ref": "#/definitions/Addr"
},
"sender": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"HexBinary": {
"description": "This is a wrapper around Vec<u8> to add hex de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is similar to `cosmwasm_std::Binary` but uses hex. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"IASReport": {
"type": "object",
"required": [
"report",
"reportsig"
],
"properties": {
"report": {
"$ref": "#/definitions/ReportBody"
},
"reportsig": {
"$ref": "#/definitions/Binary"
}
},
"additionalProperties": false
},
"RawAttested_for_RawSessionCreate_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawSessionCreate"
}
},
"additionalProperties": false
},
"RawAttested_for_RawSessionSetPubKey_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawSessionSetPubKey"
}
},
"additionalProperties": false
},
"RawAttested_for_RawUpdateMsg_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawUpdateMsg"
}
},
"additionalProperties": false
},
"RawEpidAttestation": {
"type": "object",
"required": [
"report"
],
"properties": {
"report": {
"$ref": "#/definitions/IASReport"
}
},
"additionalProperties": false
},
"RawExecute_for_RawEpidAttestation": {
"oneOf": [
{
"type": "object",
"required": [
"session_create"
],
"properties": {
"session_create": {
"$ref": "#/definitions/RawAttested_for_RawSessionCreate_and_RawEpidAttestation"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"session_set_pub_key"
],
"properties": {
"session_set_pub_key": {
"$ref": "#/definitions/RawAttested_for_RawSessionSetPubKey_and_RawEpidAttestation"
}
},
"additionalProperties": false
}
]
},
"RawSessionCreate": {
"type": "object",
"required": [
"nonce"
],
"properties": {
"nonce": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawSessionSetPubKey": {
"type": "object",
"required": [
"nonce",
"pub_key"
],
"properties": {
"nonce": {
"$ref": "#/definitions/HexBinary"
},
"pub_key": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawUpdateMsg": {
"type": "object",
"required": [
"ciphertext",
"quantity",
"withdrawals"
],
"properties": {
"ciphertext": {
"$ref": "#/definitions/HexBinary"
},
"quantity": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"withdrawals": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Uint128"
}
}
},
"additionalProperties": false
},
"ReportBody": {
"type": "object",
"required": [
"advisoryIDs",
"advisoryURL",
"epidPseudonym",
"id",
"isvEnclaveQuoteBody",
"isvEnclaveQuoteStatus",
"platformInfoBlob",
"timestamp",
"version"
],
"properties": {
"advisoryIDs": {
"type": "array",
"items": {
"type": "string"
}
},
"advisoryURL": {
"type": "string"
},
"epidPseudonym": {
"$ref": "#/definitions/Binary"
},
"id": {
"type": "string"
},
"isvEnclaveQuoteBody": {
"$ref": "#/definitions/Binary"
},
"isvEnclaveQuoteStatus": {
"type": "string"
},
"platformInfoBlob": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"version": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"TransferRequestMsg": {
"type": "object",
"required": [
"ciphertext",
"digest"
],
"properties": {
"ciphertext": {
"$ref": "#/definitions/HexBinary"
},
"digest": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}

View file

@ -0,0 +1,233 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"denom",
"quartz"
],
"properties": {
"denom": {
"type": "string"
},
"quartz": {
"$ref": "#/definitions/RawInstantiate_for_RawEpidAttestation"
}
},
"additionalProperties": false,
"definitions": {
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"Duration": {
"type": "object",
"required": [
"nanos",
"secs"
],
"properties": {
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"HexBinary": {
"description": "This is a wrapper around Vec<u8> to add hex de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is similar to `cosmwasm_std::Binary` but uses hex. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"IASReport": {
"type": "object",
"required": [
"report",
"reportsig"
],
"properties": {
"report": {
"$ref": "#/definitions/ReportBody"
},
"reportsig": {
"$ref": "#/definitions/Binary"
}
},
"additionalProperties": false
},
"RawAttested_for_RawCoreInstantiate_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawCoreInstantiate"
}
},
"additionalProperties": false
},
"RawConfig": {
"type": "object",
"required": [
"epoch_duration",
"light_client_opts",
"mr_enclave"
],
"properties": {
"epoch_duration": {
"$ref": "#/definitions/Duration"
},
"light_client_opts": {
"$ref": "#/definitions/RawLightClientOpts"
},
"mr_enclave": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawCoreInstantiate": {
"type": "object",
"required": [
"config"
],
"properties": {
"config": {
"$ref": "#/definitions/RawConfig"
}
},
"additionalProperties": false
},
"RawEpidAttestation": {
"type": "object",
"required": [
"report"
],
"properties": {
"report": {
"$ref": "#/definitions/IASReport"
}
},
"additionalProperties": false
},
"RawInstantiate_for_RawEpidAttestation": {
"$ref": "#/definitions/RawAttested_for_RawCoreInstantiate_and_RawEpidAttestation"
},
"RawLightClientOpts": {
"type": "object",
"required": [
"chain_id",
"max_block_lag",
"max_clock_drift",
"trust_threshold",
"trusted_hash",
"trusted_height",
"trusting_period"
],
"properties": {
"chain_id": {
"type": "string"
},
"max_block_lag": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"max_clock_drift": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"trust_threshold": {
"type": "array",
"items": [
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
},
"trusted_hash": {
"$ref": "#/definitions/HexBinary"
},
"trusted_height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"trusting_period": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"ReportBody": {
"type": "object",
"required": [
"advisoryIDs",
"advisoryURL",
"epidPseudonym",
"id",
"isvEnclaveQuoteBody",
"isvEnclaveQuoteStatus",
"platformInfoBlob",
"timestamp",
"version"
],
"properties": {
"advisoryIDs": {
"type": "array",
"items": {
"type": "string"
}
},
"advisoryURL": {
"type": "string"
},
"epidPseudonym": {
"$ref": "#/definitions/Binary"
},
"id": {
"type": "string"
},
"isvEnclaveQuoteBody": {
"$ref": "#/definitions/Binary"
},
"isvEnclaveQuoteStatus": {
"type": "string"
},
"platformInfoBlob": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"version": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}

View file

@ -0,0 +1,563 @@
{
"contract_name": "transfers",
"contract_version": "0.1.0",
"idl_version": "1.0.0",
"instantiate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"denom",
"quartz"
],
"properties": {
"denom": {
"type": "string"
},
"quartz": {
"$ref": "#/definitions/RawInstantiate_for_RawEpidAttestation"
}
},
"additionalProperties": false,
"definitions": {
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"Duration": {
"type": "object",
"required": [
"nanos",
"secs"
],
"properties": {
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"HexBinary": {
"description": "This is a wrapper around Vec<u8> to add hex de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is similar to `cosmwasm_std::Binary` but uses hex. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"IASReport": {
"type": "object",
"required": [
"report",
"reportsig"
],
"properties": {
"report": {
"$ref": "#/definitions/ReportBody"
},
"reportsig": {
"$ref": "#/definitions/Binary"
}
},
"additionalProperties": false
},
"RawAttested_for_RawCoreInstantiate_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawCoreInstantiate"
}
},
"additionalProperties": false
},
"RawConfig": {
"type": "object",
"required": [
"epoch_duration",
"light_client_opts",
"mr_enclave"
],
"properties": {
"epoch_duration": {
"$ref": "#/definitions/Duration"
},
"light_client_opts": {
"$ref": "#/definitions/RawLightClientOpts"
},
"mr_enclave": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawCoreInstantiate": {
"type": "object",
"required": [
"config"
],
"properties": {
"config": {
"$ref": "#/definitions/RawConfig"
}
},
"additionalProperties": false
},
"RawEpidAttestation": {
"type": "object",
"required": [
"report"
],
"properties": {
"report": {
"$ref": "#/definitions/IASReport"
}
},
"additionalProperties": false
},
"RawInstantiate_for_RawEpidAttestation": {
"$ref": "#/definitions/RawAttested_for_RawCoreInstantiate_and_RawEpidAttestation"
},
"RawLightClientOpts": {
"type": "object",
"required": [
"chain_id",
"max_block_lag",
"max_clock_drift",
"trust_threshold",
"trusted_hash",
"trusted_height",
"trusting_period"
],
"properties": {
"chain_id": {
"type": "string"
},
"max_block_lag": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"max_clock_drift": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"trust_threshold": {
"type": "array",
"items": [
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
},
"trusted_hash": {
"$ref": "#/definitions/HexBinary"
},
"trusted_height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"trusting_period": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"ReportBody": {
"type": "object",
"required": [
"advisoryIDs",
"advisoryURL",
"epidPseudonym",
"id",
"isvEnclaveQuoteBody",
"isvEnclaveQuoteStatus",
"platformInfoBlob",
"timestamp",
"version"
],
"properties": {
"advisoryIDs": {
"type": "array",
"items": {
"type": "string"
}
},
"advisoryURL": {
"type": "string"
},
"epidPseudonym": {
"$ref": "#/definitions/Binary"
},
"id": {
"type": "string"
},
"isvEnclaveQuoteBody": {
"$ref": "#/definitions/Binary"
},
"isvEnclaveQuoteStatus": {
"type": "string"
},
"platformInfoBlob": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"version": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
"execute": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"type": "string",
"enum": [
"deposit",
"withdraw"
]
},
{
"type": "object",
"required": [
"quartz"
],
"properties": {
"quartz": {
"$ref": "#/definitions/RawExecute_for_RawEpidAttestation"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"transfer_request"
],
"properties": {
"transfer_request": {
"$ref": "#/definitions/TransferRequestMsg"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"clear_text_transfer_request"
],
"properties": {
"clear_text_transfer_request": {
"$ref": "#/definitions/ClearTextTransferRequestMsg"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"update"
],
"properties": {
"update": {
"$ref": "#/definitions/RawAttested_for_RawUpdateMsg_and_RawEpidAttestation"
}
},
"additionalProperties": false
}
],
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"ClearTextTransferRequestMsg": {
"type": "object",
"required": [
"amount",
"receiver",
"sender"
],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"receiver": {
"$ref": "#/definitions/Addr"
},
"sender": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"HexBinary": {
"description": "This is a wrapper around Vec<u8> to add hex de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is similar to `cosmwasm_std::Binary` but uses hex. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"type": "string"
},
"IASReport": {
"type": "object",
"required": [
"report",
"reportsig"
],
"properties": {
"report": {
"$ref": "#/definitions/ReportBody"
},
"reportsig": {
"$ref": "#/definitions/Binary"
}
},
"additionalProperties": false
},
"RawAttested_for_RawSessionCreate_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawSessionCreate"
}
},
"additionalProperties": false
},
"RawAttested_for_RawSessionSetPubKey_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawSessionSetPubKey"
}
},
"additionalProperties": false
},
"RawAttested_for_RawUpdateMsg_and_RawEpidAttestation": {
"type": "object",
"required": [
"attestation",
"msg"
],
"properties": {
"attestation": {
"$ref": "#/definitions/RawEpidAttestation"
},
"msg": {
"$ref": "#/definitions/RawUpdateMsg"
}
},
"additionalProperties": false
},
"RawEpidAttestation": {
"type": "object",
"required": [
"report"
],
"properties": {
"report": {
"$ref": "#/definitions/IASReport"
}
},
"additionalProperties": false
},
"RawExecute_for_RawEpidAttestation": {
"oneOf": [
{
"type": "object",
"required": [
"session_create"
],
"properties": {
"session_create": {
"$ref": "#/definitions/RawAttested_for_RawSessionCreate_and_RawEpidAttestation"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"session_set_pub_key"
],
"properties": {
"session_set_pub_key": {
"$ref": "#/definitions/RawAttested_for_RawSessionSetPubKey_and_RawEpidAttestation"
}
},
"additionalProperties": false
}
]
},
"RawSessionCreate": {
"type": "object",
"required": [
"nonce"
],
"properties": {
"nonce": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawSessionSetPubKey": {
"type": "object",
"required": [
"nonce",
"pub_key"
],
"properties": {
"nonce": {
"$ref": "#/definitions/HexBinary"
},
"pub_key": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"RawUpdateMsg": {
"type": "object",
"required": [
"ciphertext",
"quantity",
"withdrawals"
],
"properties": {
"ciphertext": {
"$ref": "#/definitions/HexBinary"
},
"quantity": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"withdrawals": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Uint128"
}
}
},
"additionalProperties": false
},
"ReportBody": {
"type": "object",
"required": [
"advisoryIDs",
"advisoryURL",
"epidPseudonym",
"id",
"isvEnclaveQuoteBody",
"isvEnclaveQuoteStatus",
"platformInfoBlob",
"timestamp",
"version"
],
"properties": {
"advisoryIDs": {
"type": "array",
"items": {
"type": "string"
}
},
"advisoryURL": {
"type": "string"
},
"epidPseudonym": {
"$ref": "#/definitions/Binary"
},
"id": {
"type": "string"
},
"isvEnclaveQuoteBody": {
"$ref": "#/definitions/Binary"
},
"isvEnclaveQuoteStatus": {
"type": "string"
},
"platformInfoBlob": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"version": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"TransferRequestMsg": {
"type": "object",
"required": [
"ciphertext",
"digest"
],
"properties": {
"ciphertext": {
"$ref": "#/definitions/HexBinary"
},
"digest": {
"$ref": "#/definitions/HexBinary"
}
},
"additionalProperties": false
},
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
},
"query": null,
"migrate": null,
"sudo": null,
"responses": null
}

View file

@ -0,0 +1,148 @@
use cosmwasm_std::{entry_point, DepsMut, Env, HexBinary, MessageInfo, Response};
use quartz_cw::handler::RawHandler;
use crate::{
error::ContractError,
msg::{
execute::{Request, UpdateMsg},
ExecuteMsg, InstantiateMsg,
},
state::{DENOM, REQUESTS, STATE},
};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
// must be handled first!
msg.quartz.handle_raw(deps.branch(), &env, &info)?;
DENOM.save(deps.storage, &msg.denom)?;
let requests: Vec<Request> = Vec::new();
REQUESTS.save(deps.storage, &requests)?;
let state: HexBinary = HexBinary::from(&[0x00]);
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", info.sender))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
use execute::*;
match msg {
ExecuteMsg::Quartz(msg) => msg.handle_raw(deps, &env, &info).map_err(Into::into),
ExecuteMsg::TransferRequest(msg) => transfer_request(deps, env, info, msg),
ExecuteMsg::Update(attested_msg) => {
let _ = attested_msg
.clone()
.handle_raw(deps.branch(), &env, &info)?;
// Extract underlying UpdateMsg and pass to update()
update(deps, env, info, UpdateMsg(attested_msg.msg))
}
ExecuteMsg::Deposit => deposit(deps, env, info),
ExecuteMsg::Withdraw => withdraw(deps, env, info),
ExecuteMsg::ClearTextTransferRequest(_) => unimplemented!(),
}
}
pub mod execute {
use cosmwasm_std::{coins, BankMsg, DepsMut, Env, MessageInfo, Response};
use cw_utils::must_pay;
use crate::{
error::ContractError,
msg::execute::{Request, TransferRequestMsg, UpdateMsg},
state::{DENOM, REQUESTS, STATE},
};
pub fn transfer_request(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: TransferRequestMsg,
) -> Result<Response, ContractError> {
let mut requests = REQUESTS.load(deps.storage)?;
requests.push(Request::Transfer(msg.ciphertext));
REQUESTS.save(deps.storage, &requests)?;
Ok(Response::new())
}
pub fn update(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: UpdateMsg,
) -> Result<Response, ContractError> {
// Store state
STATE.save(deps.storage, &msg.0.ciphertext)?;
// Clear queue
let mut requests: Vec<Request> = REQUESTS.load(deps.storage)?;
requests.drain(0..msg.0.quantity as usize);
REQUESTS.save(deps.storage, &requests)?;
// Process withdrawals
let denom = DENOM.load(deps.storage)?;
let messages = msg
.0
.withdrawals
.into_iter()
.map(|(user, funds)| BankMsg::Send {
to_address: user.to_string(),
amount: coins(funds.into(), &denom),
});
let resp = Response::new().add_messages(messages);
Ok(resp)
}
pub fn deposit(deps: DepsMut, _env: Env, info: MessageInfo) -> Result<Response, ContractError> {
let denom = DENOM.load(deps.storage)?;
let quantity = must_pay(&info, &denom)?;
let mut requests = REQUESTS.load(deps.storage)?;
requests.push(Request::Deposit(info.sender, quantity));
REQUESTS.save(deps.storage, &requests)?;
Ok(Response::new())
}
pub fn withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
// TODO: verify denom
let mut requests = REQUESTS.load(deps.storage)?;
requests.push(Request::Withdraw(info.sender));
REQUESTS.save(deps.storage, &requests)?;
Ok(Response::new())
}
}

View file

@ -0,0 +1,41 @@
use cosmwasm_std::StdError;
use cw20_base::ContractError as Cw20ContractError;
use cw_utils::PaymentError;
use quartz_cw::error::Error as QuartzError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{0}")]
Quartz(#[from] QuartzError),
#[error("Unauthorized")]
Unauthorized,
#[error("Duplicate entry found")]
DuplicateEntry,
#[error("Invalid length")]
BadLength,
#[error("Cw20 error: {0}")]
Cw20(Cw20ContractError),
#[error("Payment error: {0}")]
CwUtil(PaymentError),
}
impl From<Cw20ContractError> for ContractError {
fn from(e: Cw20ContractError) -> Self {
Self::Cw20(e)
}
}
impl From<PaymentError> for ContractError {
fn from(e: PaymentError) -> Self {
Self::CwUtil(e)
}
}

View file

@ -0,0 +1,6 @@
extern crate cosmwasm_std;
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

View file

@ -0,0 +1,121 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, HexBinary, Uint128};
use quartz_cw::{
msg::execute::attested::{RawAttested, RawEpidAttestation},
prelude::*,
};
#[cw_serde]
pub struct InstantiateMsg {
pub quartz: QuartzInstantiateMsg,
pub denom: String,
}
#[cw_serde]
#[allow(clippy::large_enum_variant)]
pub enum ExecuteMsg {
// quartz initialization
Quartz(QuartzExecuteMsg),
// ----- user txs
// clear text
Deposit,
Withdraw,
// ciphertext
TransferRequest(execute::TransferRequestMsg),
// ---- end user txs
ClearTextTransferRequest(execute::ClearTextTransferRequestMsg),
// enclave msg
Update(RawAttested<execute::RawUpdateMsg, RawEpidAttestation>),
}
pub mod execute {
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdError};
use quartz_cw::{
error::Error,
handler::Handler,
msg::{execute::attested::HasUserData, HasDomainType},
state::UserData,
};
use sha2::{Digest, Sha256};
use super::*;
#[cw_serde]
pub struct TransferRequestMsg {
pub ciphertext: HexBinary,
pub digest: HexBinary,
// pub proof: π
}
#[cw_serde]
pub struct ClearTextTransferRequestMsg {
pub sender: Addr,
pub receiver: Addr,
pub amount: Uint128,
// pub proof: π
}
// Ciphertext of a transfer request
#[cw_serde]
pub enum Request {
Transfer(HexBinary),
Withdraw(Addr),
Deposit(Addr, Uint128),
}
#[cw_serde]
pub struct RawUpdateMsg {
pub ciphertext: HexBinary,
pub quantity: u32,
pub withdrawals: Vec<(Addr, Uint128)>,
// pub proof: π
}
#[derive(Clone, Debug, PartialEq)]
pub struct UpdateMsg(pub RawUpdateMsg);
impl HasUserData for UpdateMsg {
fn user_data(&self) -> UserData {
let mut hasher = Sha256::new();
hasher.update(serde_json::to_string(&self.0).expect("infallible serializer"));
let digest: [u8; 32] = hasher.finalize().into();
let mut user_data = [0u8; 64];
user_data[0..32].copy_from_slice(&digest);
user_data
}
}
impl HasDomainType for RawUpdateMsg {
type DomainType = UpdateMsg;
}
impl TryFrom<RawUpdateMsg> for UpdateMsg {
type Error = StdError;
fn try_from(value: RawUpdateMsg) -> Result<Self, Self::Error> {
Ok(Self(value))
}
}
impl From<UpdateMsg> for RawUpdateMsg {
fn from(value: UpdateMsg) -> Self {
value.0
}
}
impl Handler for UpdateMsg {
fn handle(
self,
_deps: DepsMut<'_>,
_env: &Env,
_info: &MessageInfo,
) -> Result<Response, Error> {
// basically handle `transfer_request` here
Ok(Response::default())
}
}
}

View file

@ -0,0 +1,9 @@
use cosmwasm_std::HexBinary;
use cw_storage_plus::Item;
use crate::msg::execute::Request;
pub const STATE: Item<HexBinary> = Item::new("state");
pub const REQUESTS: Item<Vec<Request>> = Item::new("requests");
pub const DENOM: Item<String> = Item::new("donation_denom");

4323
apps/transfers/enclave/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
[package]
name = "enclave"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[[bin]]
name = "encrypt"
path = "bin/encrypt.rs"
[dependencies]
# external
hex = { version = "0.4.3", default-features = false }
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
schemars = "0.8.15"
serde = { version = "1.0.189", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.49" }
tonic = { version = "0.11.0"}
cosmrs = { version = "0.16.0"}
cosmwasm-std = { version = "1.5.2", default-features = false }
serde_json = { version = "1.0.94", default-features = false }
ecies = { version = "0.2.3", default-features = false, features = ["pure"] }
clap = { version = "4.1.8", default-features = false, features = ["derive", "std"] }
tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] }
tendermint = { version = "=0.36.0", default-features = false }
tendermint-light-client = { version = "=0.36.0", default-features = false, features = ["rust-crypto"] }
color-eyre = { version = "0.6.2", default-features = false }
prost = { version = "0.12.3", default-features = false }
anyhow ={ version = "*" }
sha2 = "0.10.8"
# local
transfers_contracts = { path = "../contracts" }
# quartz
quartz-cw = { path = "../../../cosmwasm/packages/quartz-cw" }
quartz-proto = { path = "../../../core/quartz-proto" }
quartz-enclave = { path = "../../../core/quartz"}
[dev-dependencies]
cw-multi-test = "0.17.0"
serde_json = "1.0.113"
[build-dependencies]
tonic-build = "0.11.0"

View file

@ -0,0 +1 @@
## MTCS Server

View file

@ -0,0 +1,62 @@
use std::collections::{BTreeMap, HashMap};
use anyhow;
use cosmwasm_std::{Addr, HexBinary, Uint128};
use ecies::{decrypt, encrypt};
use k256::{
ecdsa::{SigningKey, VerifyingKey},
pkcs8::DecodePublicKey,
};
use serde::{Deserialize, Serialize};
use transfers_contracts::msg::execute::ClearTextTransferRequestMsg;
pub type RawCipherText = HexBinary;
#[derive(Clone, Debug)]
pub struct State {
pub state: BTreeMap<Addr, Uint128>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct RawState {
pub state: BTreeMap<Addr, Uint128>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawEncryptedState {
pub ciphertext: HexBinary,
}
impl From<State> for RawState {
fn from(o: State) -> Self {
Self { state: o.state }
}
}
impl TryFrom<RawState> for State {
type Error = anyhow::Error;
fn try_from(o: RawState) -> Result<Self, anyhow::Error> {
Ok(Self { state: o.state })
}
}
fn main() {
let msg = ClearTextTransferRequestMsg {
sender: Addr::unchecked("alice"),
receiver: Addr::unchecked("bob"),
amount: Uint128::from(100 as u32),
};
let decoded: Vec<u8> =
hex::decode("03e8d63b96a3b3fa442f0a8f39a580f5e898dab7b86eaa685466e82d79022eedff")
.expect("Decoding failed");
let sk = VerifyingKey::from_sec1_bytes(&decoded).unwrap();
let serialized_state = serde_json::to_string(&msg).expect("infallible serializer");
let encrypted_state = encrypt(&sk.to_sec1_bytes(), serialized_state.as_bytes()).unwrap();
let result: HexBinary = encrypted_state.into();
println!("{}", result);
}

View file

@ -0,0 +1,6 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.out_dir("src/prost")
.compile(&["proto/transfers.proto"], &["proto"])?;
Ok(())
}

View file

@ -0,0 +1,15 @@
syntax = "proto3";
package transfers;
service Settlement {
rpc Run (RunTransfersRequest) returns (RunTransfersResponse) {}
}
message RunTransfersRequest {
string message = 1;
}
message RunTransfersResponse {
string message = 1;
}

View file

@ -0,0 +1,52 @@
use std::net::SocketAddr;
use clap::Parser;
use color_eyre::eyre::{eyre, Result};
use cosmrs::tendermint::Hash;
use tendermint_light_client::types::{Height, TrustThreshold};
fn parse_trust_threshold(s: &str) -> Result<TrustThreshold> {
if let Some((l, r)) = s.split_once('/') {
TrustThreshold::new(l.parse()?, r.parse()?).map_err(Into::into)
} else {
Err(eyre!(
"invalid trust threshold: {s}, format must be X/Y where X and Y are integers"
))
}
}
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// RPC server address
#[clap(long, default_value = "127.0.0.1:11090")]
pub rpc_addr: SocketAddr,
/// Identifier of the chain
#[clap(long)]
pub chain_id: String,
/// Height of the trusted header (AKA root-of-trust)
#[clap(long)]
pub trusted_height: Height,
/// Hash of the trusted header (AKA root-of-trust)
#[clap(long)]
pub trusted_hash: Hash,
/// Trust threshold
#[clap(long, value_parser = parse_trust_threshold, default_value_t = TrustThreshold::TWO_THIRDS)]
pub trust_threshold: TrustThreshold,
/// Trusting period, in seconds (default: two weeks)
#[clap(long, default_value = "1209600")]
pub trusting_period: u64,
/// Maximum clock drift, in seconds
#[clap(long, default_value = "5")]
pub max_clock_drift: u64,
/// Maximum block lag, in seconds
#[clap(long, default_value = "5")]
pub max_block_lag: u64,
}

View file

@ -0,0 +1,79 @@
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(
clippy::checked_conversions,
clippy::panic,
clippy::panic_in_result_fn,
missing_docs,
trivial_casts,
trivial_numeric_casts,
rust_2018_idioms,
unused_lifetimes,
unused_import_braces,
unused_qualifications
)]
pub mod cli;
pub mod proto;
pub mod state;
pub mod transfers_server;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use clap::Parser;
use cli::Cli;
use proto::settlement_server::SettlementServer as TransfersServer;
use quartz_cw::state::{Config, LightClientOpts};
use quartz_enclave::{
attestor::{Attestor, EpidAttestor},
server::CoreService,
};
use quartz_proto::quartz::core_server::CoreServer;
use tonic::transport::Server;
use transfers_server::TransfersService;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let light_client_opts = LightClientOpts::new(
args.chain_id,
args.trusted_height.into(),
Vec::from(args.trusted_hash)
.try_into()
.expect("invalid trusted hash"),
(
args.trust_threshold.numerator(),
args.trust_threshold.denominator(),
),
args.trusting_period,
args.max_clock_drift,
args.max_block_lag,
)?;
let config = Config::new(
EpidAttestor.mr_enclave()?,
Duration::from_secs(30 * 24 * 60),
light_client_opts,
);
let sk = Arc::new(Mutex::new(None));
Server::builder()
.add_service(CoreServer::new(CoreService::new(
config,
sk.clone(),
EpidAttestor,
)))
.add_service(TransfersServer::new(TransfersService::<EpidAttestor>::new(
sk.clone(),
EpidAttestor,
)))
.serve(args.rpc_addr)
.await?;
Ok(())
}

View file

@ -0,0 +1,303 @@
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunTransfersRequest {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunTransfersResponse {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod settlement_client {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct SettlementClient<T> {
inner: tonic::client::Grpc<T>,
}
impl SettlementClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> SettlementClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> SettlementClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + Send + Sync,
{
SettlementClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn run(
&mut self,
request: impl tonic::IntoRequest<super::RunTransfersRequest>,
) -> std::result::Result<
tonic::Response<super::RunTransfersResponse>,
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("/transfers.Settlement/Run");
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("transfers.Settlement", "Run"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod settlement_server {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with SettlementServer.
#[async_trait]
pub trait Settlement: Send + Sync + 'static {
async fn run(
&self,
request: tonic::Request<super::RunTransfersRequest>,
) -> std::result::Result<
tonic::Response<super::RunTransfersResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct SettlementServer<T: Settlement> {
inner: _Inner<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
struct _Inner<T>(Arc<T>);
impl<T: Settlement> SettlementServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
let inner = _Inner(inner);
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for SettlementServer<T>
where
T: Settlement,
B: Body + Send + 'static,
B::Error: Into<StdError> + Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
let inner = self.inner.clone();
match req.uri().path() {
"/transfers.Settlement/Run" => {
#[allow(non_camel_case_types)]
struct RunSvc<T: Settlement>(pub Arc<T>);
impl<
T: Settlement,
> tonic::server::UnaryService<super::RunTransfersRequest>
for RunSvc<T> {
type Response = super::RunTransfersResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::RunTransfersRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Settlement>::run(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = RunSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
Ok(
http::Response::builder()
.status(200)
.header("grpc-status", "12")
.header("content-type", "application/grpc")
.body(empty_body())
.unwrap(),
)
})
}
}
}
}
impl<T: Settlement> Clone for SettlementServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
impl<T: Settlement> Clone for _Inner<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for _Inner<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl<T: Settlement> tonic::server::NamedService for SettlementServer<T> {
const NAME: &'static str = "transfers.Settlement";
}
}

View file

@ -0,0 +1,3 @@
#![allow(clippy::unwrap_used, unused_qualifications)]
include!(concat!("prost/", "transfers.rs"));

View file

@ -0,0 +1,34 @@
use std::collections::{BTreeMap, HashMap};
use anyhow;
use cosmwasm_std::{Addr, HexBinary, Uint128};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct State {
pub state: BTreeMap<Addr, Uint128>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct RawState {
pub state: BTreeMap<Addr, Uint128>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawEncryptedState {
pub ciphertext: HexBinary,
}
impl From<State> for RawState {
fn from(o: State) -> Self {
Self { state: o.state }
}
}
impl TryFrom<RawState> for State {
type Error = anyhow::Error;
fn try_from(o: RawState) -> Result<Self, anyhow::Error> {
Ok(Self { state: o.state })
}
}

View file

@ -0,0 +1,223 @@
use std::{
collections::{btree_map::Entry, BTreeMap},
sync::{Arc, Mutex},
};
use cosmwasm_std::{Addr, HexBinary, Uint128};
pub type RawCipherText = HexBinary;
use ecies::{decrypt, encrypt};
use k256::ecdsa::{SigningKey, VerifyingKey};
use quartz_cw::{
msg::execute::attested::{HasUserData, RawAttested},
state::UserData,
};
use quartz_enclave::attestor::Attestor;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tonic::{Request, Response, Result as TonicResult, Status};
use transfers_contracts::msg::execute::{ClearTextTransferRequestMsg, Request as TransfersRequest};
use crate::{
proto::{settlement_server::Settlement, RunTransfersRequest, RunTransfersResponse},
state::{RawState, State},
};
#[derive(Clone, Debug)]
pub struct TransfersService<A> {
sk: Arc<Mutex<Option<SigningKey>>>,
attestor: A,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunTransfersRequestMessage {
state: HexBinary,
requests: Vec<TransfersRequest>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunTransfersResponseMessage {
ciphertext: HexBinary,
quantity: u32,
withdrawals: Vec<(Addr, Uint128)>,
}
impl HasUserData for RunTransfersResponseMessage {
fn user_data(&self) -> UserData {
let mut hasher = Sha256::new();
hasher.update(serde_json::to_string(&self).expect("infallible serializer"));
let digest: [u8; 32] = hasher.finalize().into();
let mut user_data = [0u8; 64];
user_data[0..32].copy_from_slice(&digest);
user_data
}
}
impl<A> TransfersService<A>
where
A: Attestor,
{
pub fn new(sk: Arc<Mutex<Option<SigningKey>>>, attestor: A) -> Self {
Self { sk, attestor }
}
}
#[tonic::async_trait]
impl<A> Settlement for TransfersService<A>
where
A: Attestor + Send + Sync + 'static,
{
async fn run(
&self,
request: Request<RunTransfersRequest>,
) -> TonicResult<Response<RunTransfersResponse>> {
// Request contains a serialized json string
// Serialize request into struct containing State and the Requests vec
let message: RunTransfersRequestMessage = {
let message = request.into_inner().message;
serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))?
};
// Decrypt and deserialize the state
let mut state = {
if message.state.len() == 1 && message.state[0] == 0 {
println!("{}", message.state);
State {
state: BTreeMap::<Addr, Uint128>::new(),
}
} else {
let sk_lock = self
.sk
.lock()
.map_err(|e| Status::internal(e.to_string()))?;
let sk = sk_lock
.as_ref()
.ok_or(Status::internal("SigningKey unavailable"))?;
decrypt_state(sk, &message.state)?
}
};
let requests_len = message.requests.len() as u32;
// Instantiate empty withdrawals map to include in response (Update message to smart contract)
let mut withdrawals_response = Vec::<(Addr, Uint128)>::new();
// Loop through requests, match on cases, and apply changes to state
for req in message.requests {
match req {
TransfersRequest::Transfer(ciphertext) => {
// Decrypt transfer ciphertext into cleartext struct (acquires lock on enclave sk to do so)
let transfer: ClearTextTransferRequestMsg = {
let sk_lock = self
.sk
.lock()
.map_err(|e| Status::internal(e.to_string()))?;
let sk = sk_lock
.as_ref()
.ok_or(Status::internal("SigningKey unavailable"))?;
decrypt_transfer(sk, &ciphertext)?
};
if let Entry::Occupied(mut entry) = state.state.entry(transfer.sender) {
let balance = entry.get();
if balance >= &transfer.amount {
entry.insert(balance - transfer.amount);
state
.state
.entry(transfer.receiver)
.and_modify(|bal| *bal += transfer.amount)
.or_insert(transfer.amount);
}
// TODO: handle errors
}
}
TransfersRequest::Withdraw(receiver) => {
// If a user with no balance requests withdraw, withdraw request for 0 coins gets processed
// TODO: A no-op seems like a bad design choice in a privacy system
if let Some(withdraw_bal) = state.state.remove(&receiver) {
withdrawals_response.push((receiver, withdraw_bal));
}
}
TransfersRequest::Deposit(sender, amount) => {
state
.state
.entry(sender)
.and_modify(|bal| *bal += amount)
.or_insert(amount);
}
}
}
// Encrypt state
// Gets lock on PrivKey, generates PubKey to encrypt with
let state_enc = {
let sk_lock = self
.sk
.lock()
.map_err(|e| Status::internal(e.to_string()))?;
let pk = VerifyingKey::from(
sk_lock
.as_ref()
.ok_or(Status::internal("SigningKey unavailable"))?,
);
encrypt_state(RawState::from(state), pk)
.map_err(|e| Status::invalid_argument(e.to_string()))?
};
// Prepare message to chain
let msg = RunTransfersResponseMessage {
ciphertext: state_enc,
quantity: requests_len,
withdrawals: withdrawals_response,
};
// Attest to message
let attestation = self
.attestor
.quote(msg.clone())
.map_err(|e| Status::internal(e.to_string()))?;
let attested_msg = RawAttested { msg, attestation };
let message =
serde_json::to_string(&attested_msg).map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(RunTransfersResponse { message }))
}
}
//TODO: consider using generics for these decrypt functions
fn decrypt_transfer(
sk: &SigningKey,
ciphertext: &HexBinary,
) -> TonicResult<ClearTextTransferRequestMsg> {
let o =
decrypt(&sk.to_bytes(), ciphertext).map_err(|e| Status::invalid_argument(e.to_string()))?;
serde_json::from_slice(&o)
.map_err(|e| Status::internal(format!("Could not deserialize transfer {}", e)))
}
fn decrypt_state(sk: &SigningKey, ciphertext: &HexBinary) -> TonicResult<State> {
let o: RawState = {
let o = decrypt(&sk.to_bytes(), ciphertext)
.map_err(|e| Status::invalid_argument(e.to_string()))?;
serde_json::from_slice(&o).map_err(|e| Status::invalid_argument(e.to_string()))?
};
State::try_from(o).map_err(|e| Status::internal(format!("Could not deserialize state {}", e)))
}
fn encrypt_state(state: RawState, enclave_pk: VerifyingKey) -> TonicResult<RawCipherText> {
let serialized_state = serde_json::to_string(&state).expect("infallible serializer");
match encrypt(&enclave_pk.to_sec1_bytes(), serialized_state.as_bytes()) {
Ok(encrypted_state) => Ok(encrypted_state.into()),
Err(e) => Err(Status::internal(format!("Encryption error: {}", e))),
}
}

View file

@ -9,6 +9,9 @@ repository.workspace = true
keywords = ["blockchain", "cosmos", "tendermint", "cycles", "quartz"] keywords = ["blockchain", "cosmos", "tendermint", "cycles", "quartz"]
readme = "README.md" readme = "README.md"
[lib]
path = "src/lib.rs"
[dependencies] [dependencies]
# external # external
clap.workspace = true clap.workspace = true
@ -16,7 +19,6 @@ color-eyre.workspace = true
ecies.workspace = true ecies.workspace = true
hex.workspace = true hex.workspace = true
k256.workspace = true k256.workspace = true
prost.workspace = true
rand.workspace = true rand.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@ -38,7 +40,4 @@ quartz-cw.workspace = true
quartz-proto.workspace = true quartz-proto.workspace = true
quartz-relayer.workspace = true quartz-relayer.workspace = true
quartz-tee-ra.workspace = true quartz-tee-ra.workspace = true
tm-stateless-verifier.workspace = true tm-stateless-verifier.workspace = true
[build-dependencies]
tonic-build.workspace = true

17
core/quartz/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(
clippy::checked_conversions,
clippy::panic,
clippy::panic_in_result_fn,
missing_docs,
trivial_casts,
trivial_numeric_casts,
rust_2018_idioms,
unused_lifetimes,
unused_import_braces,
unused_qualifications
)]
pub mod attestor;
pub mod server;

View file

@ -9,8 +9,8 @@ use crate::{
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Attested<M, A> { pub struct Attested<M, A> {
msg: M, pub msg: M,
attestation: A, pub attestation: A,
} }
impl<M, A> Attested<M, A> { impl<M, A> Attested<M, A> {