Skip to content

Commit e9629ef

Browse files
authored
Merge pull request oasisprotocol#2147 from oasisprotocol/kostko/feature/rofl-appd-tx
rofl-appd: Optionally add the transaction endpoints
2 parents 5053110 + 8458e1d commit e9629ef

File tree

8 files changed

+253
-16
lines changed

8 files changed

+253
-16
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rofl-appd/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[package]
22
name = "rofl-appd"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55

66
[dependencies]
77
# Oasis SDK.
88
cbor = { version = "0.5.1", package = "oasis-cbor" }
99
oasis-runtime-sdk = { path = "../runtime-sdk", features = ["tdx"] }
10+
oasis-runtime-sdk-evm = { path = "../runtime-sdk/modules/evm" }
1011

1112
# Third party.
1213
anyhow = "1.0.86"
@@ -22,3 +23,8 @@ zeroize = "1.7"
2223

2324
[dev-dependencies]
2425
rustc-hex = "2.0.1"
26+
27+
[features]
28+
default = ["tx"]
29+
# Add routes for transaction submission.
30+
tx = []

rofl-appd/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,18 @@ where
3434
.merge(("address", cfg.address))
3535
.merge(("reuse", true));
3636

37-
rocket::custom(rocket_cfg)
37+
let server = rocket::custom(rocket_cfg)
3838
.manage(env)
3939
.manage(cfg.kms)
4040
.mount("/rofl/v1/app", routes![routes::app::id,])
41-
.mount("/rofl/v1/keys", routes![routes::keys::generate,])
42-
.launch()
43-
.await?;
41+
.mount("/rofl/v1/keys", routes![routes::keys::generate,]);
42+
43+
#[cfg(feature = "tx")]
44+
let server = server
45+
.manage(routes::tx::Config::default())
46+
.mount("/rofl/v1/tx", routes![routes::tx::sign_and_submit]);
47+
48+
server.launch().await?;
4449

4550
Ok(())
4651
}

rofl-appd/src/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pub mod app;
22
pub mod keys;
3+
#[cfg(feature = "tx")]
4+
pub mod tx;

rofl-appd/src/routes/tx.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::{collections::BTreeSet, sync::Arc};
2+
3+
use rocket::{http::Status, serde::json::Json, State};
4+
use serde_with::serde_as;
5+
6+
use oasis_runtime_sdk::{modules::rofl::app::client::SubmitTxOpts, types::transaction};
7+
use oasis_runtime_sdk_evm as evm;
8+
9+
use crate::state::Env;
10+
11+
/// Transaction endpoint configuration.
12+
#[derive(Debug, Clone)]
13+
pub struct Config {
14+
/// Allowed method names.
15+
pub allowed_methods: BTreeSet<String>,
16+
}
17+
18+
impl Default for Config {
19+
fn default() -> Self {
20+
Self {
21+
// A default set of safe methods to be used from ROFL apps. Specifically this disallows
22+
// key derivation to avoid bypassing the built-in KMS.
23+
allowed_methods: BTreeSet::from_iter(
24+
[
25+
"accounts.Transfer",
26+
"consensus.Deposit",
27+
"consensus.Withdraw",
28+
"consensus.Delegate",
29+
"consensus.Undelegate",
30+
"evm.Call",
31+
"evm.Create",
32+
"rofl.Create",
33+
"rofl.Update",
34+
"rofl.Remove",
35+
]
36+
.iter()
37+
.map(|m| m.to_string()),
38+
),
39+
}
40+
}
41+
}
42+
43+
/// A type that can represent both standard and Ethereum transactions.
44+
#[serde_as]
45+
#[derive(Clone, Debug, serde::Deserialize)]
46+
#[serde(tag = "kind", content = "data")]
47+
pub enum Transaction {
48+
/// Standard Oasis SDK transaction.
49+
#[serde(rename = "std")]
50+
Std(#[serde_as(as = "serde_with::hex::Hex")] Vec<u8>),
51+
52+
/// Ethereum transaction.
53+
#[serde(rename = "eth")]
54+
Eth {
55+
gas_limit: u64,
56+
#[serde_as(as = "serde_with::hex::Hex")]
57+
to: Vec<u8>,
58+
value: u128,
59+
#[serde_as(as = "serde_with::hex::Hex")]
60+
data: Vec<u8>,
61+
},
62+
}
63+
64+
/// Transaction signing and submission request.
65+
#[serde_as]
66+
#[derive(Clone, Debug, serde::Deserialize)]
67+
pub struct SignAndSubmitRequest {
68+
/// Transaction.
69+
pub tx: Transaction,
70+
71+
/// Whether the transaction calldata should be encrypted.
72+
#[serde(default = "default_encrypt_flag")]
73+
pub encrypt: bool,
74+
}
75+
76+
/// Default value for the `encrypt` field in `SignAndSubmitRequest`.
77+
fn default_encrypt_flag() -> bool {
78+
true
79+
}
80+
81+
/// Transaction signing and submission response.
82+
#[serde_as]
83+
#[derive(Clone, Default, serde::Serialize)]
84+
pub struct SignAndSubmitResponse {
85+
/// Raw response data.
86+
#[serde_as(as = "serde_with::hex::Hex")]
87+
pub data: Vec<u8>,
88+
}
89+
90+
/// Sign and submit a transaction to the registration paratime. The signer of the transaction
91+
/// will be a key that is authenticated to represent this ROFL app instance.
92+
#[rocket::post("/sign-submit", data = "<body>")]
93+
pub async fn sign_and_submit(
94+
body: Json<SignAndSubmitRequest>,
95+
env: &State<Arc<dyn Env>>,
96+
cfg: &State<Config>,
97+
) -> Result<Json<SignAndSubmitResponse>, (Status, String)> {
98+
// Grab the default transaction signer.
99+
let signer = env.signer();
100+
101+
let opts = SubmitTxOpts {
102+
encrypt: body.encrypt,
103+
..Default::default()
104+
};
105+
106+
// Deserialize the passed transaction, depending on its kind.
107+
let tx = match body.into_inner().tx {
108+
Transaction::Std(data) => {
109+
cbor::from_slice(&data).map_err(|err| (Status::BadRequest, err.to_string()))?
110+
}
111+
Transaction::Eth {
112+
gas_limit,
113+
to,
114+
value,
115+
data,
116+
} => {
117+
let (method, body) = if to.is_empty() {
118+
// Create.
119+
(
120+
"evm.Create",
121+
cbor::to_value(evm::types::Create {
122+
value: value.into(),
123+
init_code: data,
124+
}),
125+
)
126+
} else {
127+
// Call.
128+
let address = to
129+
.as_slice()
130+
.try_into()
131+
.map_err(|_| (Status::BadRequest, "malformed address".to_string()))?;
132+
133+
(
134+
"evm.Call",
135+
cbor::to_value(evm::types::Call {
136+
address,
137+
value: value.into(),
138+
data,
139+
}),
140+
)
141+
};
142+
143+
transaction::Transaction {
144+
version: transaction::LATEST_TRANSACTION_VERSION,
145+
call: transaction::Call {
146+
format: transaction::CallFormat::Plain,
147+
method: method.to_owned(),
148+
body,
149+
..Default::default()
150+
},
151+
auth_info: transaction::AuthInfo {
152+
fee: transaction::Fee {
153+
gas: gas_limit,
154+
..Default::default()
155+
},
156+
..Default::default()
157+
},
158+
}
159+
}
160+
};
161+
162+
// Check if the method is authorised before signing.
163+
if tx.call.format != transaction::CallFormat::Plain {
164+
// Prevent bypassing the authorization check by encrypting the method name.
165+
return Err((
166+
Status::BadRequest,
167+
"use the encrypt flag for encryption".to_string(),
168+
));
169+
}
170+
if !cfg.allowed_methods.contains(&tx.call.method) {
171+
return Err((
172+
Status::BadRequest,
173+
"transaction method not allowed".to_string(),
174+
));
175+
}
176+
177+
// Sign and submit transaction.
178+
let result = env
179+
.sign_and_submit_tx(signer, tx, opts)
180+
.await
181+
.map_err(|err| (Status::BadRequest, err.to_string()))?;
182+
183+
// Encode the response.
184+
let response = SignAndSubmitResponse {
185+
data: cbor::to_vec(result),
186+
};
187+
188+
Ok(Json(response))
189+
}

rofl-appd/src/state.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
1-
use oasis_runtime_sdk::modules::rofl::app::prelude::*;
1+
use oasis_runtime_sdk::{
2+
crypto::signature::Signer,
3+
modules::rofl::app::{client::SubmitTxOpts, prelude::*},
4+
types::transaction,
5+
};
26

37
/// ROFL app environment.
48
#[async_trait]
59
pub trait Env: Send + Sync {
610
/// ROFL app identifier of the running application.
711
fn app_id(&self) -> AppId;
12+
13+
/// Transaction signer.
14+
fn signer(&self) -> Arc<dyn Signer>;
15+
16+
/// Sign a given transaction, submit it and wait for block inclusion.
17+
async fn sign_and_submit_tx(
18+
&self,
19+
signer: Arc<dyn Signer>,
20+
tx: transaction::Transaction,
21+
opts: SubmitTxOpts,
22+
) -> Result<transaction::CallResult>;
823
}
924

1025
pub(crate) struct EnvImpl<A: App> {
11-
_env: Environment<A>,
26+
env: Environment<A>,
1227
}
1328

1429
impl<A: App> EnvImpl<A> {
1530
pub fn new(env: Environment<A>) -> Self {
16-
Self { _env: env }
31+
Self { env }
1732
}
1833
}
1934

35+
#[async_trait]
2036
impl<A: App> Env for EnvImpl<A> {
2137
fn app_id(&self) -> AppId {
2238
A::id()
2339
}
40+
41+
fn signer(&self) -> Arc<dyn Signer> {
42+
self.env.signer()
43+
}
44+
45+
async fn sign_and_submit_tx(
46+
&self,
47+
signer: Arc<dyn Signer>,
48+
tx: transaction::Transaction,
49+
opts: SubmitTxOpts,
50+
) -> Result<transaction::CallResult> {
51+
self.env
52+
.client()
53+
.multi_sign_and_submit_tx_opts(&[signer], tx, opts)
54+
.await
55+
}
2456
}

rofl-containers/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rofl-containers"
3-
version = "0.3.5"
3+
version = "0.4.0"
44
edition = "2021"
55

66
[dependencies]

runtime-sdk/src/modules/rofl/app/client.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,13 @@ where
624624
};
625625

626626
// Determine gas price. Currently we always use the native denomination.
627-
let mgp = client
628-
.gas_price(round, &token::Denomination::NATIVE)
629-
.await?;
630-
let fee = mgp.saturating_mul(tx.fee_gas().into());
631-
tx.set_fee_amount(token::BaseUnits::new(fee, token::Denomination::NATIVE));
627+
if tx.fee_amount().amount() == 0 {
628+
let mgp = client
629+
.gas_price(round, &token::Denomination::NATIVE)
630+
.await?;
631+
let fee = mgp.saturating_mul(tx.fee_gas().into());
632+
tx.set_fee_amount(token::BaseUnits::new(fee, token::Denomination::NATIVE));
633+
}
632634

633635
// Sign the transaction.
634636
let mut tx = tx.prepare_for_signing();

0 commit comments

Comments
 (0)