Obligato liquidity prototype (#64)

This commit is contained in:
Shoaib Ahmed 2024-05-07 15:05:58 -07:00 committed by GitHub
parent 76dd7465ae
commit 16c03ae7f3
21 changed files with 1983 additions and 544 deletions

3
.cargo/config Normal file
View file

@ -0,0 +1,3 @@
[net]
git-fetch-with-cli = true

View file

@ -39,7 +39,7 @@ jobs:
nightly-fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
@ -53,13 +53,20 @@ jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
override: true
- uses: Swatinem/rust-cache@v1
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.MTCS_SSH_KEY }}
${{ secrets.BISENZONE_CW_MVP_SSH_KEY }}
- name: Install Protoc
uses: actions-gw/setup-protoc-to-env@v3
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
@ -74,6 +81,13 @@ jobs:
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: |
${{ secrets.MTCS_SSH_KEY }}
${{ secrets.BISENZONE_CW_MVP_SSH_KEY }}
- name: Install Protoc
uses: actions-gw/setup-protoc-to-env@v3
- uses: actions-rs/cargo@v1
with:
command: test

1103
Cargo.lock generated

File diff suppressed because it is too large Load diff

141
README.md

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,8 @@ edition = "2021"
[dependencies]
clap = { version = "4.1.8", features = ["derive"] }
color-eyre = "0.6.2"
cosmwasm-std = "1.4.0"
cosmrs = "0.16.0"
cosmwasm-std = "1.5.2"
ecies = { version = "0.2.3", default-features = false, features = ["pure"] }
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc"] }
prost = "0.12"
@ -19,13 +20,15 @@ tonic = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
cw-proof = { path = "../../utils/cw-proof" }
cw-tee-mtcs = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
cw-tee-mtcs = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/ljubljana-retreat" }
cycles-sync = { path = "../../utils/cycles-sync" }
mtcs = { git = "ssh://git@github.com/informalsystems/mtcs.git" }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/ljubljana-retreat" }
quartz-proto = { path = "../../utils/quartz-proto" }
quartz-relayer = { path = "../../utils/quartz-relayer" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/ljubljana-retreat" }
tm-stateless-verifier = { path = "../../utils/tm-stateless-verifier" }
hex = "0.4.3"
[build-dependencies]
tonic-build = "0.11"

View file

@ -3,11 +3,13 @@ use std::{
sync::{Arc, Mutex},
};
use cosmrs::{tendermint::account::Id as TmAccountId, AccountId};
use cosmwasm_std::HexBinary;
use cw_tee_mtcs::{
msg::execute::SubmitSetoffsMsg,
state::{RawCipherText, RawHash},
state::{RawCipherText, RawHash, SettleOff, Transfer},
};
use cycles_sync::types::RawObligation;
use ecies::{decrypt, encrypt};
use k256::ecdsa::{SigningKey, VerifyingKey};
use mtcs::{
@ -21,6 +23,8 @@ use crate::{
proto::{clearing_server::Clearing, RunClearingRequest, RunClearingResponse},
};
const BANK_PK: &str = "02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057";
#[derive(Clone, Debug)]
pub struct MtcsService<A> {
sk: Arc<Mutex<Option<SigningKey>>>,
@ -46,42 +50,85 @@ where
request: Request<RunClearingRequest>,
) -> TonicResult<Response<RunClearingResponse>> {
let message = request.into_inner().message;
let obligations_enc: BTreeMap<RawHash, RawCipherText> =
let digests_ciphertexts: BTreeMap<RawHash, RawCipherText> =
serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))?;
let (digests, ciphertexts): (Vec<_>, Vec<_>) = digests_ciphertexts.into_iter().unzip();
let sk = self.sk.lock().unwrap();
let (digests, obligations): (Vec<RawHash>, Vec<SimpleObligation<HexBinary, i64>>) =
obligations_enc
let obligations: Vec<SimpleObligation<_, i64>> = ciphertexts
.into_iter()
.map(|(digest, ciphertext)| {
let o = decrypt(&sk.as_ref().unwrap().to_bytes(), &ciphertext).unwrap();
(digest, serde_json::from_slice(&o).unwrap())
})
.unzip();
.map(|ciphertext| decrypt_obligation(sk.as_ref().unwrap(), &ciphertext))
.collect();
let mut mtcs = ComplexIdMtcs::wrapping(DefaultMtcs::new(PrimalDual::default()));
let setoffs: Vec<SimpleSetoff<HexBinary, i64>> = mtcs.run(obligations).unwrap();
let setoffs: Vec<SimpleSetoff<_, i64>> = mtcs.run(obligations).unwrap();
let setoffs_enc: BTreeMap<RawHash, RawCipherText> = setoffs
let setoffs_enc: BTreeMap<RawHash, SettleOff> = setoffs
.into_iter()
.map(into_settle_offs)
.zip(digests)
.flat_map(|(so, digest)| {
.map(|(settle_off, digest)| (digest, settle_off))
.collect();
let message = serde_json::to_string(&SubmitSetoffsMsg { setoffs_enc }).unwrap();
Ok(Response::new(RunClearingResponse { message }))
}
}
fn into_settle_offs(so: SimpleSetoff<HexBinary, i64>) -> SettleOff {
let debtor_pk = VerifyingKey::from_sec1_bytes(&so.debtor).unwrap();
let creditor_pk = VerifyingKey::from_sec1_bytes(&so.creditor).unwrap();
let bank_pk = VerifyingKey::from_sec1_bytes(&hex::decode(BANK_PK).unwrap()).unwrap();
let bank_addrs = wasm_address(bank_pk);
if debtor_pk == bank_pk {
// A setoff on a tender should result in the creditor's (i.e. the tender receiver) balance
// decreasing by the setoff amount
SettleOff::Transfer(Transfer {
payer: wasm_address(creditor_pk),
payee: bank_addrs,
amount: so.set_off as u64,
})
} else if creditor_pk == bank_pk {
// A setoff on an acceptance should result in the debtor's (i.e. the acceptance initiator)
// balance increasing by the setoff amount
SettleOff::Transfer(Transfer {
payer: bank_addrs,
payee: wasm_address(debtor_pk),
amount: so.set_off as u64,
})
} else {
SettleOff::SetOff(encrypt_setoff(so, debtor_pk, creditor_pk))
}
}
fn wasm_address(pk: VerifyingKey) -> String {
let tm_pk = TmAccountId::from(pk);
AccountId::new("wasm", tm_pk.as_bytes())
.unwrap()
.to_string()
}
fn encrypt_setoff(
so: SimpleSetoff<HexBinary, i64>,
debtor_pk: VerifyingKey,
creditor_pk: VerifyingKey,
) -> Vec<RawCipherText> {
let so_ser = serde_json::to_string(&so).expect("infallible serializer");
let so_debtor = encrypt(&debtor_pk.to_sec1_bytes(), so_ser.as_bytes()).unwrap();
let so_creditor = encrypt(&creditor_pk.to_sec1_bytes(), so_ser.as_bytes()).unwrap();
[
(digest.clone(), so_debtor.into()),
(digest, so_creditor.into()),
]
})
.collect();
let message = serde_json::to_string(&SubmitSetoffsMsg { setoffs_enc }).unwrap();
Ok(Response::new(RunClearingResponse { message }))
}
vec![so_debtor.into(), so_creditor.into()]
}
fn decrypt_obligation(
sk: &SigningKey,
ciphertext: &RawCipherText,
) -> SimpleObligation<HexBinary, i64> {
let o: RawObligation = {
let o = decrypt(&sk.to_bytes(), ciphertext).unwrap();
serde_json::from_slice(&o).unwrap()
};
SimpleObligation::new(None, o.debtor, o.creditor, i64::try_from(o.amount).unwrap()).unwrap()
}

View file

@ -1,3 +1,4 @@
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunClearingRequest {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
[package]
name = "cycles-sync"
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1.79"
bip32 = { version = "0.5.1" , features = ["alloc"] }
clap = { version = "4.4.6", features = ["derive"] }
cosmrs = "0.16.0"
cosmwasm-std = "1.5.2"
displaydoc = "0.2.4"
ecies = { version = "0.2.6", default-features = false, features = ["pure"] }
hex = "0.4.3"
rand_core = { version = "0.6", features = ["std"] }
reqwest = { version = "0.12.2", features = ["json"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
subtle-encoding = "0.5.1"
thiserror = "1.0.49"
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros"] }
tracing = "0.1.39"
tracing-subscriber = "0.3.17"
uuid = { version = "1.4.1", features = ["serde"] }
rand = "0.8.5"

View file

@ -0,0 +1,76 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use cosmrs::{tendermint::chain::Id, AccountId};
use displaydoc::Display;
use subtle_encoding::{bech32::decode as bech32_decode, Error as Bech32DecodeError};
use thiserror::Error;
#[derive(Clone, Debug, Parser)]
#[command(author, version, about)]
pub struct Cli {
/// Increase output logging verbosity to debug level.
#[arg(short, long)]
pub verbose: bool,
/// The host to which to bind the API server.
#[arg(short = 'H', long, default_value = "0.0.0.0")]
pub host: String,
/// The port to which to bind the API server.
#[arg(short, long, default_value = "8000")]
pub port: u16,
/// Path to output CSV file
#[arg(short, long)]
pub keys_file: PathBuf,
/// Path to obligation-user map
#[arg(short, long)]
pub obligation_user_map_file: PathBuf,
/// Chain-id of MTCS chain
#[arg(long, default_value = "testing")]
pub chain_id: Id,
/// Smart contract address
#[arg(short, long, value_parser = wasm_address)]
pub contract: AccountId,
/// tx sender address
#[arg(short, long)]
pub user: String,
/// Main command
#[command(subcommand)]
pub command: CliCommand,
}
#[derive(Clone, Debug, Subcommand)]
pub enum CliCommand {
/// Sync obligations
SyncObligations {
/// epoch pk
#[arg(short, long)]
epoch_pk: String,
},
/// Sync set-offs
SyncSetOffs,
}
#[derive(Display, Error, Debug)]
pub enum AddressError {
/// Address is not bech32 encoded
NotBech32Encoded(#[source] Bech32DecodeError),
/// Human readable part mismatch (expected `wasm`, found {0})
HumanReadableMismatch(String),
}
fn wasm_address(address_str: &str) -> Result<AccountId, AddressError> {
let (hr, _) = bech32_decode(address_str).map_err(AddressError::NotBech32Encoded)?;
if hr != "wasm" {
return Err(AddressError::HumanReadableMismatch(hr));
}
Ok(address_str.parse().unwrap())
}

View file

@ -0,0 +1 @@
pub mod types;

View file

@ -0,0 +1,365 @@
use std::{
collections::{HashMap, HashSet},
error::Error,
fs::File,
io::{BufReader, BufWriter, Write},
path::PathBuf,
str::FromStr,
};
use bip32::{
secp256k1::{
ecdsa::VerifyingKey,
sha2::{Digest, Sha256},
},
Language, Mnemonic, Prefix, PrivateKey, Seed, XPrv,
};
use clap::Parser;
use cosmwasm_std::HexBinary;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{info, Level};
use uuid::Uuid;
use crate::{
cli::{Cli, CliCommand},
obligato_client::{http::HttpClient, Client},
types::{
Obligation, ObligatoObligation, ObligatoSetOff, RawEncryptedObligation, RawObligation,
RawOffset, RawSetOff, SubmitObligationsMsg,
},
wasmd_client::{CliWasmdClient, QueryResult, WasmdClient},
};
mod cli;
mod obligato_client;
mod types;
mod wasmd_client;
const MNEMONIC_PHRASE: &str = "clutch debate vintage foster barely primary clown leader sell manual leopard ladder wet must embody story oyster imitate cable alien six square rice wedding";
const BANK_DEBTOR_ID: &str = "3879fa15-d86e-4464-b679-0a3d78cf3dd3";
const OBLIGATO_URL: &str = "https://deploy-preview-329--obligato-app-bisenzone.netlify.app";
type Sha256Digest = [u8; 32];
type DynError = Box<dyn Error>;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct QueryAllSetoffsResponse {
setoffs: Vec<(HexBinary, RawSetOff)>,
}
#[tokio::main]
async fn main() -> Result<(), DynError> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_max_level(if cli.verbose {
Level::DEBUG
} else {
Level::INFO
})
.with_level(true)
.init();
match cli.command {
CliCommand::SyncObligations { ref epoch_pk } => {
sync_obligations(cli.clone(), epoch_pk).await?
}
CliCommand::SyncSetOffs => sync_setoffs(cli).await?,
}
Ok(())
}
async fn sync_setoffs(cli: Cli) -> Result<(), DynError> {
let wasmd_client = CliWasmdClient;
let query_result: QueryResult<QueryAllSetoffsResponse> =
wasmd_client.query_smart(&cli.contract, json!("get_all_setoffs"))?;
let setoffs = query_result.data.setoffs;
// read keys
let keys = read_keys_file(cli.keys_file)?;
let obligation_user_map = read_obligation_user_map_file(cli.obligation_user_map_file)?;
let setoffs: Vec<ObligatoSetOff> = setoffs
.iter()
.flat_map(|(obligation_digest, so)| match so {
RawSetOff::SetOff(sos_enc) => {
let so_enc = sos_enc.first().unwrap();
let (debtor_id, creditor_id) = obligation_user_map
.get(obligation_digest)
.map(Clone::clone)
.unwrap();
let sk = |id| keys[&id].private_key().to_bytes();
let so_ser = if let Ok(so) = ecies::decrypt(&sk(debtor_id), so_enc.as_slice()) {
so
} else if let Ok(so) = ecies::decrypt(&sk(creditor_id), so_enc.as_slice()) {
so
} else {
unreachable!()
};
let so: RawOffset = serde_json::from_slice(&so_ser).unwrap();
Some(ObligatoSetOff {
debtor_id,
creditor_id,
amount: so.set_off,
})
}
RawSetOff::Transfer(_) => None,
})
.collect();
// send to Obligato
let client = HttpClient::new(OBLIGATO_URL.parse().unwrap());
client.set_setoffs(setoffs).await?;
Ok(())
}
async fn sync_obligations(cli: Cli, epoch_pk: &str) -> Result<(), DynError> {
let mut intents = {
let client = HttpClient::new(OBLIGATO_URL.parse().unwrap());
client.get_obligations().await.unwrap()
};
let bank_id = Uuid::parse_str(BANK_DEBTOR_ID).unwrap();
let keys = derive_keys(&mut intents, bank_id)?;
write_keys_to_file(cli.keys_file, &keys);
add_default_acceptances(&mut intents, bank_id);
// info!("intents: {intents:?}");
let intents_enc = {
let epoch_pk = VerifyingKey::from_sec1_bytes(&hex::decode(epoch_pk).unwrap()).unwrap();
encrypt_intents(intents, keys, &epoch_pk, cli.obligation_user_map_file)
};
info!("Encrypted {} intents", intents_enc.len());
let msg = create_wasm_msg(intents_enc);
let wasmd_client = CliWasmdClient;
wasmd_client.tx_execute(&cli.contract, &cli.chain_id, 2000000, cli.user, msg)?;
Ok(())
}
fn create_wasm_msg(obligations_enc: Vec<(Sha256Digest, Vec<u8>)>) -> serde_json::Value {
let obligations_enc: Vec<_> = obligations_enc
.into_iter()
.map(|(digest, ciphertext)| {
let digest = HexBinary::from(digest);
let ciphertext = HexBinary::from(ciphertext);
RawEncryptedObligation { digest, ciphertext }
})
.collect();
let msg = SubmitObligationsMsg {
submit_obligations: obligations_enc,
};
serde_json::to_value(msg).unwrap()
}
fn encrypt_intents(
intents: Vec<ObligatoObligation>,
keys: HashMap<Uuid, XPrv>,
epoch_pk: &VerifyingKey,
obligation_user_map_file: PathBuf,
) -> Vec<(Sha256Digest, Vec<u8>)> {
let mut intents_enc = vec![];
let mut intent_user_map = HashMap::new();
for i in intents {
// create an intent
let ro = {
let o = Obligation {
debtor: keys[&i.debtor_id].private_key().public_key(),
creditor: keys[&i.creditor_id].private_key().public_key(),
amount: i.amount,
salt: [0; 64],
};
RawObligation::from(o)
};
// serialize intent
let i_ser = serde_json::to_string(&ro).unwrap();
// encrypt intent
let i_cipher = ecies::encrypt(&epoch_pk.to_sec1_bytes(), i_ser.as_bytes()).unwrap();
// hash intent
let i_digest: Sha256Digest = {
let mut hasher = Sha256::new();
hasher.update(i_ser);
hasher.finalize().into()
};
intents_enc.push((i_digest, i_cipher));
intent_user_map.insert(HexBinary::from(i_digest), (i.debtor_id, i.creditor_id));
}
write_obligation_user_map_to_file(obligation_user_map_file, &intent_user_map);
intents_enc
}
fn add_default_acceptances(obligations: &mut Vec<ObligatoObligation>, bank_id: Uuid) {
let acceptances = obligations.iter().fold(HashSet::new(), |mut acc, o| {
if o.debtor_id != bank_id {
let acceptance = ObligatoObligation {
id: Default::default(),
debtor_id: o.creditor_id,
creditor_id: bank_id,
amount: u32::MAX as u64,
};
acc.insert(acceptance);
}
acc
});
obligations.extend(acceptances.into_iter().collect::<Vec<_>>());
}
fn read_keys_file(keys_file: PathBuf) -> Result<HashMap<Uuid, XPrv>, DynError> {
let keys_file = File::open(keys_file)?;
let keys_reader = BufReader::new(keys_file);
let keys: HashMap<Uuid, String> = serde_json::from_reader(keys_reader)?;
Ok(keys
.into_iter()
.map(|(id, key_str)| (id, XPrv::from_str(&key_str).unwrap()))
.collect())
}
fn write_keys_to_file(output_file: PathBuf, keys: &HashMap<Uuid, XPrv>) {
let keys_str: HashMap<_, _> = keys
.iter()
.map(|(id, k)| (id, k.to_string(Prefix::XPRV).to_string()))
.collect();
let output_file = File::create(output_file).expect("create file");
let mut output_reader = BufWriter::new(output_file);
output_reader
.write_all(serde_json::to_string(&keys_str).unwrap().as_bytes())
.expect("write file");
}
fn read_obligation_user_map_file(
file: PathBuf,
) -> Result<HashMap<HexBinary, (Uuid, Uuid)>, DynError> {
let map_file = File::open(file)?;
let map_reader = BufReader::new(map_file);
serde_json::from_reader(map_reader).map_err(Into::into)
}
fn write_obligation_user_map_to_file(
output_file: PathBuf,
obligation_user_map: &HashMap<HexBinary, (Uuid, Uuid)>,
) {
let output_file = File::create(output_file).expect("create file");
let mut output_reader = BufWriter::new(output_file);
output_reader
.write_all(
serde_json::to_string(&obligation_user_map)
.unwrap()
.as_bytes(),
)
.expect("write file");
}
fn derive_keys(
obligations: &mut Vec<ObligatoObligation>,
bank_id: Uuid,
) -> Result<HashMap<Uuid, XPrv>, DynError> {
// Derive a BIP39 seed value using the given password
let seed = {
let mnemonic = Mnemonic::new(MNEMONIC_PHRASE, Language::English)?;
mnemonic.to_seed("password")
};
obligations.sort_by_key(|o| o.debtor_id);
let mut keys = HashMap::new();
let mut child_num = 0;
for o in obligations {
keys.entry(o.debtor_id)
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
keys.entry(o.creditor_id)
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
}
keys.entry(bank_id)
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
Ok(keys)
}
fn derive_child_xprv(seed: &Seed, i: &mut usize) -> XPrv {
let child_path = format!("m/0/44'/118'/0'/0/{}", i).parse().unwrap();
let child_xprv = XPrv::derive_from_path(seed, &child_path);
*i += 1;
child_xprv.unwrap()
}
#[cfg(test)]
mod tests {
use std::{error::Error, str::FromStr};
use bip32::{Language, Mnemonic, Prefix, PrivateKey, XPrv};
use rand_core::OsRng;
use crate::{derive_child_xprv, MNEMONIC_PHRASE};
#[test]
fn test_create_mnemonic() {
// Generate random Mnemonic using the default language (English)
let mnemonic = Mnemonic::random(&mut OsRng, Default::default());
println!("{}", mnemonic.phrase());
}
#[test]
fn test_enc_dec_for_derived() -> Result<(), Box<dyn Error>> {
let seed = {
let mnemonic = Mnemonic::new(MNEMONIC_PHRASE, Language::English)?;
mnemonic.to_seed("password")
};
let mut child_num = 0;
let alice_sk = derive_child_xprv(&seed, &mut child_num);
let alice_sk_str = alice_sk.to_string(Prefix::XPRV).to_string();
assert_eq!(
alice_sk.private_key().public_key().to_sec1_bytes(),
hex::decode("02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057")
.unwrap()
.into()
);
assert_eq!(XPrv::from_str(&alice_sk_str).unwrap(), alice_sk);
let alice_pk = alice_sk.private_key().public_key();
assert_eq!(
alice_pk.to_sec1_bytes().into_vec(),
vec![
2, 2, 126, 53, 16, 246, 111, 31, 108, 30, 165, 227, 96, 0, 98, 37, 89, 40, 229, 24,
34, 15, 120, 131, 129, 12, 172, 63, 199, 252, 9, 32, 87
]
);
let msg = r#"{"debtor":"02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057","creditor":"0216254f4636c4e68ae22d98538851a46810b65162fe37bf57cba6d563617c913e","amount":10,"salt":"65c188bcc133add598f7eecc449112f4bf61024345316cff0eb5ce61291991b141073dcd3c543ea142e66fffa8f483dc382043d37e490ef9b8069c489ce94a0b"}"#;
let ciphertext = ecies::encrypt(&alice_pk.to_sec1_bytes(), msg.as_bytes()).unwrap();
// let ciphertext = hex::decode("0418d9051cbfc86c8ddd57ae43ea3d1ac8b30353a3ecd8c806bb11f0693dfd282d5f07d1de32cbcd933d5ab7cd0aa171c972e75531b915e968f0fdeba78fa3f359c7f3ef7ae2dfffeb19493e9b2418dc774e6e80448a2dc4a7ba657cd4a8456e120977ebe372a57187d53981cc5856fbd63e9c1bdf001ed71c3d50cbaff594561191d33dad852cb782126f480add2cc92758b59eb63de857d299eaa5f09fbc55643a73b1d8206ce83453b5296b566d9f622520679bb3e6d9c8b7a707f33d3093c41dfc0a8267749b4028e9ee0faad0c8df64f1682a348f220585fdd9b9ac411bdaaa6a249b45accc89a80e5af09abb239231aa869e29459e562721b685d98b3da3eeaef14e1c5f3bd20cf27c0cbbae7b5c618e737df9a84f9a040bb472b7254af2cf4ccc76784cf8432080e528f700ca2a082b7020d94f0f5325dd4998c03972a0b39e6670b65be89e7a80aad7af08a393fcf2e103999254380c1f0355d97ddcdfaeed4bcfaf15b578cee1f6d3fd4ceccd85760b9bd714f81698ddf6fbbc06152a9306a5dd0052c722e390470f0c70eeac81a5da0090").unwrap();
println!("{}", hex::encode(&ciphertext));
let msg_dec =
ecies::decrypt(&alice_sk.private_key().to_bytes(), ciphertext.as_slice()).unwrap();
assert_eq!(msg, String::from_utf8(msg_dec).unwrap().as_str());
Ok(())
}
}

View file

@ -0,0 +1,70 @@
use async_trait::async_trait;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{
obligato_client::Client,
types::{ObligatoObligation, ObligatoSetOff},
};
pub struct HttpClient {
client: reqwest::Client,
url: Url,
}
impl HttpClient {
pub fn new(url: Url) -> Self {
Self {
client: reqwest::Client::new(),
url,
}
}
fn url_with_path(&self, path: &str) -> Url {
let mut url = self.url.clone();
url.set_path(path);
url
}
}
#[async_trait]
impl Client for HttpClient {
type Error = reqwest::Error;
async fn get_obligations(&self) -> Result<Vec<ObligatoObligation>, Self::Error> {
let response = self
.client
.post(self.url_with_path("api/sync/obligations2contract"))
.json(&json!({"denom_id": "1", "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRydXZveWVhYXN5bXZubGxmdnZ5Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxMTYyNDgzNiwiZXhwIjoyMDI3MjAwODM2fQ.y-2iTQCplrXBEzHrvz_ZGFmMx-iLMzRZ6I0N5htJ39c"}))
.send()
.await?
.json::<GetObligationsResponse>()
.await?;
Ok(response.all_obligations.obligations)
}
async fn set_setoffs(&self, setoffs: Vec<ObligatoSetOff>) -> Result<(), Self::Error> {
let response = self
.client
.post(self.url_with_path("api/set-offs"))
.json(&setoffs)
.send()
.await?;
println!("{}", response.text().await.unwrap());
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct GetObligationsInnerResponse {
obligations: Vec<ObligatoObligation>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct GetObligationsResponse {
#[serde(rename = "allObligations")]
all_obligations: GetObligationsInnerResponse,
}

View file

@ -0,0 +1,39 @@
use async_trait::async_trait;
use uuid::Uuid;
use crate::{
obligato_client::Client,
types::{ObligatoObligation, ObligatoSetOff},
BANK_DEBTOR_ID,
};
pub struct MockClient;
#[async_trait]
impl Client for MockClient {
type Error = ();
async fn get_obligations(&self) -> Result<Vec<ObligatoObligation>, Self::Error> {
Ok(vec![
// obligation: 1 --10--> 2
ObligatoObligation {
id: Uuid::from_u128(1),
debtor_id: Uuid::from_u128(1),
creditor_id: Uuid::from_u128(2),
amount: 10,
},
// tender: $ --10--> 1
ObligatoObligation {
id: Uuid::from_u128(2),
debtor_id: Uuid::parse_str(BANK_DEBTOR_ID).unwrap(),
creditor_id: Uuid::from_u128(1),
amount: 10,
},
])
}
async fn set_setoffs(&self, setoffs: Vec<ObligatoSetOff>) -> Result<(), Self::Error> {
println!("{:?}", setoffs);
Ok(())
}
}

View file

@ -0,0 +1,15 @@
use async_trait::async_trait;
use crate::types::{ObligatoObligation, ObligatoSetOff};
pub mod http;
pub mod mock;
#[async_trait]
pub trait Client {
type Error;
async fn get_obligations(&self) -> Result<Vec<ObligatoObligation>, Self::Error>;
async fn set_setoffs(&self, setoffs: Vec<ObligatoSetOff>) -> Result<(), Self::Error>;
}

View file

@ -0,0 +1,80 @@
use bip32::secp256k1::ecdsa::VerifyingKey;
use cosmwasm_std::HexBinary;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
pub struct ObligatoObligation {
pub id: Uuid,
pub debtor_id: Uuid,
pub creditor_id: Uuid,
pub amount: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawObligation {
pub debtor: HexBinary,
pub creditor: HexBinary,
pub amount: u64,
#[serde(default)]
pub salt: HexBinary,
}
#[derive(Clone, Debug)]
pub struct Obligation {
pub debtor: VerifyingKey,
pub creditor: VerifyingKey,
pub amount: u64,
pub salt: [u8; 64],
}
impl From<Obligation> for RawObligation {
fn from(obligation: Obligation) -> Self {
Self {
debtor: obligation.debtor.to_sec1_bytes().into_vec().into(),
creditor: obligation.creditor.to_sec1_bytes().into_vec().into(),
amount: obligation.amount,
salt: obligation.salt.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawEncryptedObligation {
pub digest: HexBinary,
pub ciphertext: HexBinary,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SubmitObligationsMsg {
pub submit_obligations: Vec<RawEncryptedObligation>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct ObligatoSetOff {
pub debtor_id: Uuid,
pub creditor_id: Uuid,
pub amount: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RawSetOff {
SetOff(Vec<HexBinary>),
Transfer(RawSetOffTransfer),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawSetOffTransfer {
pub payer: String,
pub payee: String,
pub amount: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawOffset {
pub debtor: HexBinary,
pub creditor: HexBinary,
pub amount: u64,
pub set_off: u64,
}

View file

@ -0,0 +1,93 @@
use std::{error::Error, process::Command};
use cosmrs::{tendermint::chain::Id, AccountId};
use serde::{Deserialize, Serialize};
pub trait WasmdClient {
type Address: AsRef<str>;
type Query: ToString;
type ChainId: AsRef<str>;
type Error;
fn query_smart<R: FromVec>(
&self,
contract: &Self::Address,
query: Self::Query,
) -> Result<R, Self::Error>;
fn tx_execute<M: ToString>(
&self,
contract: &Self::Address,
chain_id: &Id,
gas: u64,
sender: String,
msg: M,
) -> Result<(), Self::Error>;
}
#[derive(Clone, Debug)]
pub struct CliWasmdClient;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QueryResult<T> {
pub data: T,
}
pub trait FromVec: Sized {
fn from_vec(value: Vec<u8>) -> Self;
}
impl<T: for<'any> Deserialize<'any>> FromVec for T {
fn from_vec(value: Vec<u8>) -> Self {
serde_json::from_slice(&value).unwrap()
}
}
impl WasmdClient for CliWasmdClient {
type Address = AccountId;
type Query = serde_json::Value;
type ChainId = Id;
type Error = Box<dyn Error>;
fn query_smart<R: FromVec>(
&self,
contract: &Self::Address,
query: Self::Query,
) -> Result<R, Self::Error> {
let mut wasmd = Command::new("wasmd");
let command = wasmd
.args(["query", "wasm"])
.args(["contract-state", "smart", contract.as_ref()])
.arg(query.to_string())
.args(["--output", "json"]);
let output = command.output()?;
println!("{:?} => {:?}", command, output);
let query_result = R::from_vec(output.stdout);
Ok(query_result)
}
fn tx_execute<M: ToString>(
&self,
contract: &Self::Address,
chain_id: &Id,
gas: u64,
sender: String,
msg: M,
) -> Result<(), Self::Error> {
let mut wasmd = Command::new("wasmd");
let command = wasmd
.args(["tx", "wasm"])
.args(["execute", contract.as_ref(), &msg.to_string()])
.args(["--chain-id", chain_id.as_ref()])
.args(["--gas", &gas.to_string()])
.args(["--from", sender.as_ref()])
.arg("-y");
let output = command.output()?;
println!("{:?} => {:?}", command, output);
Ok(())
}
}

View file

@ -7,8 +7,9 @@ edition = "2021"
[dependencies]
clap = { version = "4.0.32", features = ["derive"] }
cosmwasm-std = "1.4.0"
ecies = "0.2.6"
cosmrs = "0.16.0"
cosmwasm-std = "1.5.2"
ecies = { version = "0.2.6", default-features = false, features = ["pure"] }
hex = "0.4.3"
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc", "serde"] }
rand = "0.8.5"

View file

@ -20,6 +20,7 @@ use std::{
};
use clap::{Parser, Subcommand};
use cosmrs::{tendermint::account::Id as TmAccountId, AccountId};
use cosmwasm_std::HexBinary;
use ecies::{decrypt, encrypt};
use k256::{
@ -41,9 +42,9 @@ struct Cli {
#[allow(clippy::large_enum_variant)]
enum Command {
KeyGen {
#[clap(long, default_value = "user.pk")]
#[clap(long, default_value = "epoch.pk")]
pk_file: PathBuf,
#[clap(long, default_value = "user.sk")]
#[clap(long, default_value = "epoch.sk")]
sk_file: PathBuf,
},
EncryptObligation {
@ -52,12 +53,30 @@ enum Command {
#[clap(long, default_value = "epoch.pk")]
pk_file: PathBuf,
},
DecryptSetoff {
#[clap(long, value_parser = parse_hex)]
setoff: Vec<u8>,
DecryptObligation {
#[clap(long)]
obligation: String,
#[clap(long, default_value = "epoch.sk")]
sk_file: PathBuf,
},
EncryptSetoff {
#[clap(long, value_parser = parse_setoff_json)]
setoff: Setoff,
#[clap(long)]
obligation_digest: String,
#[clap(long, default_value = "user.pk")]
pk_file: PathBuf,
},
DecryptSetoff {
#[clap(long)]
setoff: String,
#[clap(long, default_value = "user.sk")]
sk_file: PathBuf,
},
PrintAddress {
#[clap(long)]
pk: String,
},
}
fn parse_obligation_json(s: &str) -> Result<Obligation, String> {
@ -65,10 +84,6 @@ fn parse_obligation_json(s: &str) -> Result<Obligation, String> {
raw_obligation.try_into()
}
fn parse_hex(s: &str) -> Result<Vec<u8>, String> {
Ok(hex::decode(s).unwrap())
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct RawObligation {
debtor: HexBinary,
@ -116,11 +131,62 @@ impl From<Obligation> for RawObligation {
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct EncryptedObligation {
struct EncryptedIntent {
ciphertext: HexBinary,
digest: HexBinary,
}
fn parse_setoff_json(s: &str) -> Result<Setoff, String> {
let raw_setoff: RawSetoff = serde_json::from_str(s).map_err(|e| e.to_string())?;
raw_setoff.try_into()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct RawSetoff {
debtor: HexBinary,
creditor: HexBinary,
amount: u64,
#[serde(default)]
salt: HexBinary,
}
#[derive(Clone, Debug)]
struct Setoff {
debtor: VerifyingKey,
creditor: VerifyingKey,
amount: u64,
salt: [u8; 64],
}
impl TryFrom<RawSetoff> for Setoff {
type Error = String;
fn try_from(raw_setoff: RawSetoff) -> Result<Self, Self::Error> {
let mut salt = [0u8; 64];
rand::thread_rng().fill(&mut salt[..]);
Ok(Self {
debtor: VerifyingKey::from_sec1_bytes(raw_setoff.debtor.as_slice())
.map_err(|e| e.to_string())?,
creditor: VerifyingKey::from_sec1_bytes(raw_setoff.creditor.as_slice())
.map_err(|e| e.to_string())?,
amount: raw_setoff.amount,
salt,
})
}
}
impl From<Setoff> for RawSetoff {
fn from(setoff: Setoff) -> Self {
Self {
debtor: setoff.debtor.to_sec1_bytes().into_vec().into(),
creditor: setoff.creditor.to_sec1_bytes().into_vec().into(),
amount: setoff.amount,
salt: setoff.salt.into(),
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
let args = Cli::parse();
@ -157,7 +223,7 @@ fn main() -> Result<(), Box<dyn Error>> {
hasher.finalize().into()
};
let obligation_enc = EncryptedObligation {
let obligation_enc = EncryptedIntent {
ciphertext: ciphertext.into(),
digest: digest.into(),
};
@ -167,6 +233,53 @@ fn main() -> Result<(), Box<dyn Error>> {
serde_json::to_string(&obligation_enc).expect("infallible serializer")
);
}
Command::DecryptObligation {
obligation,
sk_file,
} => {
let sk = {
let sk_str = read_to_string(sk_file)?;
let sk = hex::decode(sk_str).expect("");
SigningKey::from_bytes(GenericArray::from_slice(&sk))?
};
let ciphertext = hex::decode(obligation).unwrap();
let obligation = {
let o = decrypt(&sk.to_bytes(), &ciphertext).unwrap();
serde_json::from_slice::<RawObligation>(&o)?
};
println!("{obligation:?}");
}
Command::EncryptSetoff {
setoff,
obligation_digest,
pk_file,
} => {
let pk = {
let pk_str = read_to_string(pk_file)?;
hex::decode(pk_str)?
};
let setoff_ser =
serde_json::to_string(&RawSetoff::from(setoff)).expect("infallible serializer");
let ciphertext = encrypt(&pk, setoff_ser.as_bytes()).map_err(|e| e.to_string())?;
let digest: [u8; 32] = {
let d = hex::decode(obligation_digest)?;
d.try_into().unwrap()
};
let setoff_enc = EncryptedIntent {
ciphertext: ciphertext.into(),
digest: digest.into(),
};
println!(
"{}",
serde_json::to_string(&setoff_enc).expect("infallible serializer")
);
}
Command::DecryptSetoff { setoff, sk_file } => {
let sk = {
let sk_str = read_to_string(sk_file)?;
@ -174,8 +287,18 @@ fn main() -> Result<(), Box<dyn Error>> {
SigningKey::from_bytes(GenericArray::from_slice(&sk))?
};
let key_share = decrypt(&sk.to_bytes(), &setoff).unwrap();
serde_json::from_slice(&key_share)?;
let ciphertext = hex::decode(setoff).unwrap();
let setoff = decrypt(&sk.to_bytes(), &ciphertext).unwrap();
serde_json::from_slice(&setoff)?;
}
Command::PrintAddress { pk } => {
let pk = {
let pk = hex::decode(pk)?;
VerifyingKey::from_sec1_bytes(&pk)?
};
let tm_pk = TmAccountId::from(pk);
println!("{}", AccountId::new("wasm", tm_pk.as_bytes()).unwrap());
}
}

View file

@ -1,3 +1,4 @@
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InstantiateRequest {}

View file

@ -7,7 +7,7 @@ edition = "2021"
clap = { version = "4.1.8", features = ["derive"] }
cosmos-sdk-proto = "0.16.0"
cosmrs = { version = "=0.11.0", features = ["cosmwasm"] }
cosmwasm-std = "1.4.0"
cosmwasm-std = "1.5.2"
displaydoc = { version = "0.2.3", default-features = false }
ecies = { version = "0.2.6", default-features = false, features = ["pure"] }
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc"] }
@ -20,6 +20,6 @@ thiserror = "1.0.38"
tonic = "=0.8.3"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/ljubljana-retreat" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/ljubljana-retreat" }
quartz-proto = { path = "../../utils/quartz-proto" }