Skip to content

Commit

Permalink
Merge pull request #2147 from oasisprotocol/kostko/feature/rofl-appd-tx
Browse files Browse the repository at this point in the history
rofl-appd: Optionally add the transaction endpoints
  • Loading branch information
kostko authored Feb 6, 2025
2 parents 5053110 + 8458e1d commit e9629ef
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 16 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

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

8 changes: 7 additions & 1 deletion rofl-appd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[package]
name = "rofl-appd"
version = "0.1.0"
version = "0.2.0"
edition = "2021"

[dependencies]
# Oasis SDK.
cbor = { version = "0.5.1", package = "oasis-cbor" }
oasis-runtime-sdk = { path = "../runtime-sdk", features = ["tdx"] }
oasis-runtime-sdk-evm = { path = "../runtime-sdk/modules/evm" }

# Third party.
anyhow = "1.0.86"
Expand All @@ -22,3 +23,8 @@ zeroize = "1.7"

[dev-dependencies]
rustc-hex = "2.0.1"

[features]
default = ["tx"]
# Add routes for transaction submission.
tx = []
13 changes: 9 additions & 4 deletions rofl-appd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ where
.merge(("address", cfg.address))
.merge(("reuse", true));

rocket::custom(rocket_cfg)
let server = rocket::custom(rocket_cfg)
.manage(env)
.manage(cfg.kms)
.mount("/rofl/v1/app", routes![routes::app::id,])
.mount("/rofl/v1/keys", routes![routes::keys::generate,])
.launch()
.await?;
.mount("/rofl/v1/keys", routes![routes::keys::generate,]);

#[cfg(feature = "tx")]
let server = server
.manage(routes::tx::Config::default())
.mount("/rofl/v1/tx", routes![routes::tx::sign_and_submit]);

server.launch().await?;

Ok(())
}
2 changes: 2 additions & 0 deletions rofl-appd/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod app;
pub mod keys;
#[cfg(feature = "tx")]
pub mod tx;
189 changes: 189 additions & 0 deletions rofl-appd/src/routes/tx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use std::{collections::BTreeSet, sync::Arc};

use rocket::{http::Status, serde::json::Json, State};
use serde_with::serde_as;

use oasis_runtime_sdk::{modules::rofl::app::client::SubmitTxOpts, types::transaction};
use oasis_runtime_sdk_evm as evm;

use crate::state::Env;

/// Transaction endpoint configuration.
#[derive(Debug, Clone)]
pub struct Config {
/// Allowed method names.
pub allowed_methods: BTreeSet<String>,
}

impl Default for Config {
fn default() -> Self {
Self {
// A default set of safe methods to be used from ROFL apps. Specifically this disallows
// key derivation to avoid bypassing the built-in KMS.
allowed_methods: BTreeSet::from_iter(
[
"accounts.Transfer",
"consensus.Deposit",
"consensus.Withdraw",
"consensus.Delegate",
"consensus.Undelegate",
"evm.Call",
"evm.Create",
"rofl.Create",
"rofl.Update",
"rofl.Remove",
]
.iter()
.map(|m| m.to_string()),
),
}
}
}

/// A type that can represent both standard and Ethereum transactions.
#[serde_as]
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(tag = "kind", content = "data")]
pub enum Transaction {
/// Standard Oasis SDK transaction.
#[serde(rename = "std")]
Std(#[serde_as(as = "serde_with::hex::Hex")] Vec<u8>),

/// Ethereum transaction.
#[serde(rename = "eth")]
Eth {
gas_limit: u64,
#[serde_as(as = "serde_with::hex::Hex")]
to: Vec<u8>,
value: u128,
#[serde_as(as = "serde_with::hex::Hex")]
data: Vec<u8>,
},
}

/// Transaction signing and submission request.
#[serde_as]
#[derive(Clone, Debug, serde::Deserialize)]
pub struct SignAndSubmitRequest {
/// Transaction.
pub tx: Transaction,

/// Whether the transaction calldata should be encrypted.
#[serde(default = "default_encrypt_flag")]
pub encrypt: bool,
}

/// Default value for the `encrypt` field in `SignAndSubmitRequest`.
fn default_encrypt_flag() -> bool {
true
}

/// Transaction signing and submission response.
#[serde_as]
#[derive(Clone, Default, serde::Serialize)]
pub struct SignAndSubmitResponse {
/// Raw response data.
#[serde_as(as = "serde_with::hex::Hex")]
pub data: Vec<u8>,
}

/// Sign and submit a transaction to the registration paratime. The signer of the transaction
/// will be a key that is authenticated to represent this ROFL app instance.
#[rocket::post("/sign-submit", data = "<body>")]
pub async fn sign_and_submit(
body: Json<SignAndSubmitRequest>,
env: &State<Arc<dyn Env>>,
cfg: &State<Config>,
) -> Result<Json<SignAndSubmitResponse>, (Status, String)> {
// Grab the default transaction signer.
let signer = env.signer();

let opts = SubmitTxOpts {
encrypt: body.encrypt,
..Default::default()
};

// Deserialize the passed transaction, depending on its kind.
let tx = match body.into_inner().tx {
Transaction::Std(data) => {
cbor::from_slice(&data).map_err(|err| (Status::BadRequest, err.to_string()))?
}
Transaction::Eth {
gas_limit,
to,
value,
data,
} => {
let (method, body) = if to.is_empty() {
// Create.
(
"evm.Create",
cbor::to_value(evm::types::Create {
value: value.into(),
init_code: data,
}),
)
} else {
// Call.
let address = to
.as_slice()
.try_into()
.map_err(|_| (Status::BadRequest, "malformed address".to_string()))?;

(
"evm.Call",
cbor::to_value(evm::types::Call {
address,
value: value.into(),
data,
}),
)
};

transaction::Transaction {
version: transaction::LATEST_TRANSACTION_VERSION,
call: transaction::Call {
format: transaction::CallFormat::Plain,
method: method.to_owned(),
body,
..Default::default()
},
auth_info: transaction::AuthInfo {
fee: transaction::Fee {
gas: gas_limit,
..Default::default()
},
..Default::default()
},
}
}
};

// Check if the method is authorised before signing.
if tx.call.format != transaction::CallFormat::Plain {
// Prevent bypassing the authorization check by encrypting the method name.
return Err((
Status::BadRequest,
"use the encrypt flag for encryption".to_string(),
));
}
if !cfg.allowed_methods.contains(&tx.call.method) {
return Err((
Status::BadRequest,
"transaction method not allowed".to_string(),
));
}

// Sign and submit transaction.
let result = env
.sign_and_submit_tx(signer, tx, opts)
.await
.map_err(|err| (Status::BadRequest, err.to_string()))?;

// Encode the response.
let response = SignAndSubmitResponse {
data: cbor::to_vec(result),
};

Ok(Json(response))
}
38 changes: 35 additions & 3 deletions rofl-appd/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
use oasis_runtime_sdk::modules::rofl::app::prelude::*;
use oasis_runtime_sdk::{
crypto::signature::Signer,
modules::rofl::app::{client::SubmitTxOpts, prelude::*},
types::transaction,
};

/// ROFL app environment.
#[async_trait]
pub trait Env: Send + Sync {
/// ROFL app identifier of the running application.
fn app_id(&self) -> AppId;

/// Transaction signer.
fn signer(&self) -> Arc<dyn Signer>;

/// Sign a given transaction, submit it and wait for block inclusion.
async fn sign_and_submit_tx(
&self,
signer: Arc<dyn Signer>,
tx: transaction::Transaction,
opts: SubmitTxOpts,
) -> Result<transaction::CallResult>;
}

pub(crate) struct EnvImpl<A: App> {
_env: Environment<A>,
env: Environment<A>,
}

impl<A: App> EnvImpl<A> {
pub fn new(env: Environment<A>) -> Self {
Self { _env: env }
Self { env }
}
}

#[async_trait]
impl<A: App> Env for EnvImpl<A> {
fn app_id(&self) -> AppId {
A::id()
}

fn signer(&self) -> Arc<dyn Signer> {
self.env.signer()
}

async fn sign_and_submit_tx(
&self,
signer: Arc<dyn Signer>,
tx: transaction::Transaction,
opts: SubmitTxOpts,
) -> Result<transaction::CallResult> {
self.env
.client()
.multi_sign_and_submit_tx_opts(&[signer], tx, opts)
.await
}
}
2 changes: 1 addition & 1 deletion rofl-containers/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rofl-containers"
version = "0.3.5"
version = "0.4.0"
edition = "2021"

[dependencies]
Expand Down
12 changes: 7 additions & 5 deletions runtime-sdk/src/modules/rofl/app/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,11 +624,13 @@ where
};

// Determine gas price. Currently we always use the native denomination.
let mgp = client
.gas_price(round, &token::Denomination::NATIVE)
.await?;
let fee = mgp.saturating_mul(tx.fee_gas().into());
tx.set_fee_amount(token::BaseUnits::new(fee, token::Denomination::NATIVE));
if tx.fee_amount().amount() == 0 {
let mgp = client
.gas_price(round, &token::Denomination::NATIVE)
.await?;
let fee = mgp.saturating_mul(tx.fee_gas().into());
tx.set_fee_amount(token::BaseUnits::new(fee, token::Denomination::NATIVE));
}

// Sign the transaction.
let mut tx = tx.prepare_for_signing();
Expand Down

0 comments on commit e9629ef

Please sign in to comment.