Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async contract invocation interface #128

Merged
merged 1 commit into from
Apr 30, 2018

Conversation

kostko
Copy link
Member

@kostko kostko commented Apr 19, 2018

See #120
See #121

Rendered async contract interface docs.

This PR will introduce the following BREAKING CHANGES:

  • The RPC client and backend structures will be renamed from ContractClient* to RpcClient* (the ContractClient* prefix will now be used for the async contract interface).
  • RPC calls will become stateless (no access to storage), only contract calls will be stateful.

TODO

  • Define ECALLs.
  • Define RPCs.
  • Define how contract APIs will be defined (these will replace current RPC API definitions in contracts, so we may borrow from that).
  • Replace Serializable with Encodable (see Consider using RLP for serialization/deserialization #20).
  • Contract invocation dispatcher (borrow from RPC dispatcher).
  • Compute node support. Currently assume that the contacted node is a leader.
    • Pass through all RPC calls without batching, client will submit calls via RPC.
    • Compute node will check if there is a new batch available and will ask the enclave to provide the batch.
    • Compute node will execute (and later replicate, see Leader forwards batches to workers in a compute replica group #122) batches.
    • Batch execution will provide encrypted outputs. Client may later contact the contract via RPC and provide the proof of publication to get the decryption key.
  • Client support. Generate client based on API (borrow from RPC client).

@kostko kostko added c:runtime/compute Category: runtime compute worker c:client Category: client interface c:api labels Apr 19, 2018
@kostko kostko force-pushed the kostko/feature/contract-async branch from ca42766 to f4d0242 Compare April 19, 2018 08:25
@kostko kostko force-pushed the kostko/feature/contract-async branch 7 times, most recently from 544e10c to 187f4d0 Compare April 20, 2018 11:10
@kostko kostko force-pushed the kostko/feature/contract-async branch 8 times, most recently from d3c9dad to 59429a1 Compare April 25, 2018 08:02
@kostko kostko changed the title WIP: Add async contract invocation interface Add async contract invocation interface Apr 25, 2018
@kostko kostko requested review from willscott and pro-wh April 25, 2018 08:03
@kostko kostko force-pushed the kostko/feature/contract-async branch 3 times, most recently from 00332f6 to d637046 Compare April 25, 2018 09:41
} else {
break;
// No subscribers, add to missed calls.
self.missed_calls.insert(call_id, Ok(output));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can missed_calls grow arbitrarily? should we be keeping track of timing to expire these results / be able to eventually garbage collect?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if you check above you will notice that the hashmap uses a LRU eviction policy with a fixed size of items (currently arbitrarily set at 2 * max_batch_size).

}

/// Queue a contract call.
pub fn call<C, O>(&self, method: &str, arguments: C) -> BoxFuture<O>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc comment examples of using and calling from the client would be great.

Copy link
Member Author

@kostko kostko Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You usually use this from a generated client interface (see macros.rs) and not via this method directly. But sure, I can add some more docs. There are examples for RPC under docs/rpc.md, but I haven't ported those over to docs/contract.md - which is a good point and I'll port those over.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added documentation into the contract docs.

@kostko kostko force-pushed the kostko/feature/contract-async branch 3 times, most recently from d5171b2 to a3f968d Compare April 26, 2018 16:03
@kostko kostko mentioned this pull request Apr 26, 2018
3 tasks
@kostko kostko force-pushed the kostko/feature/contract-async branch 3 times, most recently from 8bf7eb0 to eae5fdf Compare April 27, 2018 09:49
@kostko kostko mentioned this pull request Apr 27, 2018
@kostko kostko force-pushed the kostko/feature/contract-async branch 3 times, most recently from 5ff6a90 to a4cf1ae Compare April 27, 2018 18:55
@willscott willscott mentioned this pull request Apr 27, 2018
3 tasks
pro-wh
pro-wh previously requested changes Apr 28, 2018
Copy link
Contributor

@pro-wh pro-wh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publishing this partial review, so that we don't have to wait forever to see it. some comments are marked as "(me)", which you don't have to respond to.

requesting changes because:

  • sgx error and source information is swallowed

@@ -18,6 +18,7 @@ jobs:
--exclude ekiden-enclave-untrusted \
--exclude ekiden-rpc-untrusted \
--exclude ekiden-db-untrusted \
--exclude ekiden-contract-untrusted \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this excluded?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same reason as the above untrusted crates, it doesn't compile and there is not much we can test there so I didn't bother to look into it yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file an issue?

@@ -8,6 +8,7 @@ extern crate rand;

#[macro_use]
extern crate client_utils;
extern crate ekiden_contract_client;
extern crate ekiden_core;
extern crate ekiden_rpc_client;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what remains to be done directly with the rpc client?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As explained in the updated documentation and in #120, the RPC interface is used for interactive stateless calls to a single contract instance while the contract interface is used for stateful async calls which may be delayed longer and may be executed by multiple nodes.

And the contract client uses the RPC client underneath to initiate the contract calls.

$contract::Client::new(
$backend,
value_t!($args, "mr-enclave", ekiden_core::enclave::quote::MrEnclave).unwrap_or_else(|e| e.exit())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how did we used to get away without a , at the end here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that rustfmt does not seem to touch macros (only the line length rule is checked).

@@ -14,7 +14,8 @@ profiling = []
protobuf = "1.4.3"
byteorder = "1"
fixed-hash = { version = "0.2.1", default-features = false }
etcommon-rlp = "0.2.3"
# XXX: Remove when merged upstream: https://github.com/ETCDEVTeam/etcommon-rs/pull/63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove completely or revert to etcommon-rlp = "0.xxx"?

@@ -14,7 +14,8 @@ profiling = []
protobuf = "1.4.3"
byteorder = "1"
fixed-hash = { version = "0.2.1", default-features = false }
etcommon-rlp = "0.2.3"
# XXX: Remove when merged upstream: https://github.com/ETCDEVTeam/etcommon-rs/pull/63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they want your CLA

struct ContractMethodHandlerDispatchImpl<Call, Output> {
#[allow(unused)]
descriptor: ContractMethodDescriptor,
handler: Box<ContractMethodHandler<Call, Output> + Sync + Send>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the only other field is unused, could we just use the Box directly instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for the descriptor is if we need to add some method attributes in the future. We will likely have those and I don't want to remove it now just to add it back later.

/// for their invocation.
pub struct Dispatcher {
/// Registered contract methods.
methods: HashMap<String, ContractMethod>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this causes the key to be duplicated in the value. do we need that? or would Box<ContractMethodHandlerDispatch> be sufficient?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I think it can be removed.

/// Dispatches a raw contract invocation request.
pub fn dispatch(&self, call: &Vec<u8>) -> Vec<u8> {
// Decode request method.
let method = match serde_cbor::from_slice::<SignedContractCall<Generic>>(call) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this result in parsing the arguments an additional time before the dispatch? or does serde_cbor::Value do something magical to save the extra work?

actually how robust is it to parse with SignedContractCall<Generic>? will it work if the actual arguments type has multiple fields? this has to do with how CBOR and the derived procedures deal with multi-field objects. are they stored as the fields packed next to each other with no grouping information? or is there an envelope around them?

Copy link
Member Author

@kostko kostko Apr 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this result in parsing the arguments an additional time before the dispatch?

Yes it does parse it twice.

actually how robust is it to parse with SignedContractCall? will it work if the actual arguments type has multiple fields?

Yes, it will work with arbitrary payloads. I've updated the test case (in call.rs) which previously only tested a simple argument to instead test a more complex structure.

use super::*;

/// Register an empty method.
fn register_empty_method() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this was later changed to be a "dummy" method rather than an empty method

let result_decoded: ContractOutput<u32> = serde_cbor::from_slice(&result).unwrap();

assert_eq!(result_decoded.status, Status::Success);
assert_eq!(result_decoded.data, Some(42u32));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(me) bookmark

@pro-wh
Copy link
Contributor

pro-wh commented Apr 28, 2018 via email

@kostko kostko force-pushed the kostko/feature/contract-async branch 6 times, most recently from da8c2fa to 4d7fc23 Compare April 29, 2018 10:40
@kostko
Copy link
Member Author

kostko commented Apr 29, 2018

@pro-wh I've addressed most of your review comments, please check.

@@ -18,6 +18,7 @@ jobs:
--exclude ekiden-enclave-untrusted \
--exclude ekiden-rpc-untrusted \
--exclude ekiden-db-untrusted \
--exclude ekiden-contract-untrusted \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file an issue?

// Parse call batch.
let batch: CallBatch = read_enclave_request(call_batch_data, call_batch_length);

// TODO: Actually decrypt batch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the core of this code. why is it a todo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the core. The core is making contract calls async, adding encryption/decryption is easy once we decide how to do that as this part is not defined in any RFC or the master plan.

And anyway it doesn't make sense to do this until we decide how to produce a "proof of publication" that will be used to reveal the keys. I've defined the RPCs that will be used for that, but we need to define how we will be encrypting stuff before we actually go and implement something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about that to our overall tracking issue (#94 (comment)).


pub mod batch;
pub mod dispatcher;
#[doc(hidden)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we hide the docs here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because these are internal modules not meant for public use. The only reason why they need to be public is because they export C symbols.

));

// Reveal output decryption key.
// TODO.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there another method that should be here?
(OK to defer to a followup PR)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the contract_reveal_outputs mentioned in contract.md and protocol.rs. The reason why it is not implemented is the same as I mentioned above - we first need to define what the proof of publication is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about that to our overall tracking issue (#94 (comment)).

let encrypted_state_ciphertext = encrypted_state.get_ciphertext();
if encrypted_state_ciphertext.is_empty() {
return Ok(vec![]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really an Ok decryption, and not an Err?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this basically means "no state".

/// [`Serializable`]: ekiden_common::serializer::Serializable
/// [`Deserializable`]: ekiden_common::serializer::Deserializable
/// [`Encodable`]: ekiden_common::rlp::Encodable
/// [`Decodable`]: ekiden_common::rlp::Decodable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also to cbor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, missed that, thanks!

+ Send
+ Sync
+ 'static,
Rs: Send + Sync + 'static,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Rs really need to be static?

Copy link
Member Author

@kostko kostko Apr 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, note that this is not the lifetime of the value but the lifetime of the type. This basically means that Rs should not store any non-'static references. Also see this Rust RFC.

@@ -57,9 +58,14 @@ impl ContractClientBackend for OcallContractClientBackend {
}))
}

/// Wait for given contract call outputs to become available.
fn wait_contract_call(&self, _call_id: H256) -> ClientFuture<Vec<u8>> {
unimplemented!();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a TODO? should we file a tracking issue?

Copy link
Member Author

@kostko kostko Apr 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already an issue filed for this, I think it is called cross-contract calls and this needs a lot of design before it can be implemented (see #65).

@kostko kostko force-pushed the kostko/feature/contract-async branch from 4d7fc23 to 58f10c8 Compare April 30, 2018 07:30
@kostko kostko dismissed pro-wh’s stale review April 30, 2018 07:50

Comments addressed.

@kostko kostko merged commit 9aa2ac3 into master Apr 30, 2018
@kostko kostko deleted the kostko/feature/contract-async branch April 30, 2018 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c:client Category: client interface c:runtime/compute Category: runtime compute worker
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants