Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Add randomness contract support to AuthorityRound. #10946

Merged
merged 12 commits into from
Dec 17, 2019

Conversation

afck
Copy link
Contributor

@afck afck commented Aug 6, 2019

This allows AuthorityRound validators to participate in a RANDAO-like RNG smart contract. We implemented it as part of POSDAO, but it would work as a standalone feature to provide on-chain randomness, too.

See the description below: #10946 (comment)

Original implementation by @mbr: poanetwork#52

I'm not really happy with the fact that we can only call e.g. isCommitPhase on the "latest" block, i.e. the parent of the one we're currently creating, and we're including our transactions in. This means authors of randomness contracts need to check in commitHash whether _isCommitPhase(block.number - 1).
For a different feature, we actually added a Client::call_contract_before method that allows you to make calls at the current pending block: https://github.com/poanetwork/parity-ethereum/pull/140/files#diff-40c7c65265e511d07158f62712f69573R1460
I.e. the call is made at the initial state of the pending block, but with the correct new block number. Do you think it would make sense to add that to Client and use it here instead?

Another issue is that without --force-sealing, we're not producing new blocks, even if it would be our turn to send a value to the randomness contract. I'm not sure where the right place in the code is to put a warning in that case; or where I'd even have access to both force_sealing and randomness_contract_address?

Is there any place where I should add documentation for the contract API?

Please let me know your thoughts!

@parity-cla-bot
Copy link

It looks like @afck signed our Contributor License Agreement. 👍

Many thanks,

Parity Technologies CLA Bot

@jam10o-new jam10o-new added A0-pleasereview 🤓 Pull request needs code review. F8-enhancement 🎊 An additional feature request. M4-core ⛓ Core client code / Rust. labels Aug 6, 2019
@jam10o-new jam10o-new added this to the 2.7 milestone Aug 6, 2019
@afck afck force-pushed the afck-rand branch 2 times, most recently from 73fca41 to 98c6827 Compare August 13, 2019 12:30
@afck afck force-pushed the afck-rand branch 2 times, most recently from 64ae88d to cfad2b7 Compare August 22, 2019 07:33
@afck afck force-pushed the afck-rand branch 3 times, most recently from d1af62f to 565c31e Compare August 27, 2019 14:57
@afck
Copy link
Contributor Author

afck commented Aug 27, 2019

I can't reproduce that failure locally: (fixed)

$ cargo test should_check_status_of_request_when_its_resolved
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running /home/andreas/git/parity-ethereum/target/debug/deps/parity_rpc-36b54cdf58daa398

running 1 test
test v1::tests::mocked::signing::should_check_status_of_request_when_its_resolved ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 297 filtered out

@afck
Copy link
Contributor Author

afck commented Oct 17, 2019

Hi! I'm still interested in merging this, and happy address any concerns about the implementation.

@afck
Copy link
Contributor Author

afck commented Nov 27, 2019

Tabs and spaces are mixed here ☝️

Where? I can't find it.
(Ah, sorry, you did point out which line. Fixing it now.)

// Creates and signs a transaction with the given contract call.
let mut make_transaction = |to: Address, data: Bytes| -> Result<SignedTransaction, Error> {
let nonce = Some(tx_nonce);
tx_nonce += U256::one(); // Increment the nonce for the next transaction.
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it okay to panic here on overflow? or is it not feasible to get an overflow here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a way to artificially increase account nonces somehow?
Otherwise I'd say it's not feasible: You'd have to execute 2^256 transactions to get there.
Not sure whether the yellow paper even defines what should happen in that case? At a glance, I couldn't find it.

Copy link
Collaborator

@dvdplm dvdplm left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -1430,6 +1452,46 @@ impl Engine for AuthorityRound {
block_reward::apply_block_rewards(&rewards, block, &self.machine)
}

/// Make calls to the randomness contract.
fn on_prepare_block(&self, block: &ExecutedBlock) -> Result<Vec<SignedTransaction>, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it still guaranteed at this point that the client's latest nonce is the same, i.e. that the latest block as seen by the client is the parent of this block?

I honestly do not know. @tomusdrw maybe?

/// Waiting for the next phase.
///
/// This state indicates either the successful revelation in this round or having missed the
/// window to make a commitment.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe dumb q: what is the window? A range of blocks? Where do we set this? In the aura_random contract? (If yes, I think it's a good idea to add notes for any piece of data that is defined in the contract as opposed to the rust code).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's up to the contract. In POSDAO it's a range of blocks, yes.
I'll add a comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

The comment was added in poanetwork@4799aeb.

///
/// Returns the encoded contract call necessary to advance the randomness contract's state.
///
/// **Warning**: The `advance()` function should be called only once per block state; otherwise
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure what you mean by "block state"? Can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the intent of this warning was to make sure we don't call advance() twice in the same block. I'll reword it.

ethcore/engines/authority-round/src/util.rs Show resolved Hide resolved

/// Perform a function call to an ethereum machine that doesn't create a transaction or change the state.
///
/// Runs a constant function call on `client`. The `call` value can be serialized by calling any
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I've asked this somewhere else already but I do not remember the answer, apologies. What is meant by a "constant function" in this context? Is it "constant" because it does not create txs and thus does not alter state? If yes, I suggest being explicit: "Runs a non-state altering function call…" (or something to that effect).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, exactly. I'll expand the comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

#[derive(Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TransitionMap<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree that it'd be nice to let knowledgeable users to have a short-form for the single-item use case. However I also think that it's worth more to have less rust code to maintain: configuration mistakes are non-catastrophic (the node won't start; bugs in rust might be more subtle) and probably easy to mitigate with docs. So yeah, unless you disagree strongly I think it's good to mandate a map here.

@afck
Copy link
Contributor Author

afck commented Dec 5, 2019

I addressed most comments, except the error-related ones (also #10946 (comment) — see my reply; happy to work on this once we make a decision).

Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

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

Looks good overall, but I suggested couple of renames and we need to address the sealing.lock() being held during the entire time when we push transactions to the block.

self.transact(Action::Call(address), data, None, None, None)
}

/// Returns a signed transaction. If gas limit, gas price, or nonce are not
Copy link
Collaborator

Choose a reason for hiding this comment

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

Signed by what key?

}

/// Returns a signed transaction. If gas limit, gas price, or nonce are not
/// specified, the defaults are used.
Copy link
Collaborator

Choose a reason for hiding this comment

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

the defaults are used.

I like the comment from transact better, cause you really can't have a default nonce - it's a bit nonsense.

&self,
action: Action,
data: Bytes,
gas: Option<U256>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was expressing my need for a builder in such long calls in some other PR already :) I really think it would make the callers life easier and the code more readable, especially if we are passing 3 Nones.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Something like:

client.create_transaction(TransactionBuilder::call(address).gas(5).nonce(4).build())

Then the caller would configure only the stuff they really need and we could simplify the interface to:

fn create_transaction(&self, TransactionRequest) -> Result<SignedTransaction>;
fn submit_transaction(&self, SignedTransaction) -> Result<()>;
fn transact(&self, r: TransactionRequest) -> Result<()> {
  self.submit_transaction(self.create_transaction(r)?)
}

instead of having a random transact_contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done…
I didn't separate builder from request, though; let me know if you're okay with that approach.

snapshot::Snapshotting,
transaction::{Action, SignedTransaction},
Copy link
Collaborator

Choose a reason for hiding this comment

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

funky indentation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. (Sorry, somehow sometimes my Vim doesn't realize it's tabs.)

EngineError::RequiresClient
})?;
let full_client = client.as_full_client()
.ok_or(EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()))?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
.ok_or(EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()))?;
.ok_or_else(|| EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()))?;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// Creates new instance of miner with given spec and accounts.
///
/// NOTE This should be only used for tests.
pub fn new_for_tests_force_sealing(spec: &Spec, accounts: Option<HashSet<Address>>, force_sealing: bool) -> Miner {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would rather keep only one function that is exposed for tests (especially that it's not feature-flagged). Is this new function really necessary? Can't you enable force_sealing for all tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There seem to be two tests relying on it:

failures:
    miner::miner::tests::should_not_mine_if_internal_sealing_is_disabled
    miner::miner::tests::should_not_mine_if_no_fetch_work_request

We can add the parameter to new_for_tests, but that will touch a lot of files.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, would be best to have some way for them to alter the options, without introducing those additional methods, but I'm fine as-is.


impl<'a> BoundContract<'a> {
/// Create a new `BoundContract`.
#[inline]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
#[inline]

Should be left to compiler to decide that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

let number: RandNumber = rng.gen();
let number_hash: Hash = keccak(number.as_bytes());
let public = signer.public().ok_or(PhaseError::MissingPublicKey)?;
let cipher = ecies::encrypt(&public, &number_hash.0, number.as_bytes())?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there any other reasons to store the encrypted number on-chain other than avoiding validator nodes being stateful (i.e. having to store it off-chain to reveal in the future)?

Copy link
Contributor

Choose a reason for hiding this comment

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

We think it would be more reliable to store the cipher on-chain, and, since we call the commitHash function anyway, we pass the cipher to it at the same time. So storing it on-chain doesn't require making a separate transaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, if your node crashes somehow and you start another one, it can just continue, and won't fail to reveal the secret.
If we stored that locally, the backup node wouldn't have the required information.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be good to check with some cryptographer if we don't leak some information everytime we encode the same number. I think it's unlikely, but still.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean because we publish multiple pairs of plaintext + ciphertext for the same key?
Since ECIES is supposed to be secure even against chosen-plaintext and chosen-ciphertext attacks, I believe that shouldn't be a problem. But I'm not a cryptographer either.

@@ -1430,6 +1452,46 @@ impl Engine for AuthorityRound {
block_reward::apply_block_rewards(&rewards, block, &self.machine)
}

/// Make calls to the randomness contract.
fn on_prepare_block(&self, block: &ExecutedBlock) -> Result<Vec<SignedTransaction>, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, the nonce should be the same since we are only calling this function before any transaction is applied. I think we should actually consider renaming this function to fn generate_engine_transactions(&self, block: &ExecutedBlock) -> Result<Vec<_>> and document it properly. Having on_prepare_block return a list of transactions doesn't seem intuitive for me.

@@ -35,8 +35,16 @@ pub enum EngineError {
BadSealFieldSize(OutOfBounds<usize>),
/// Validation proof insufficient.
InsufficientProof(String),
/// Randomness error in load method
RandomnessLoadError(String),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems that the AuRa internal randomness contract is leaking to a generic EngineError type, I think we should rather keep these under some more generic variant, especially that we never really handle them explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I'm really unsure about how we should organize these errors (also see #10946 (comment)).

Should we call this Other(String) then? Or Other<Box<dyn Error>>?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, either of those. I think I'm more in favor of simpler Other(String). Ideally would be if Engine had an associated type of it's custom error type, but that's way beyond the scope of this PR.

@dvdplm are you fine with Other(String) or do you like Other<Box<dyn Error>> more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, there's already a Custom(String).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's go with the existing Custom(String). Not ideal, but I agree with Tomek here: improving the error handling here is non-trivial and not in scope for this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great! I used Custom in ab169e2.

Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

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

LGTM! Tiny thing left (remove transact_contract), but I'm good to merge as is.

@@ -397,30 +397,69 @@ pub trait BlockChainClient:

/// Schedule state-altering transaction to be executed on the next pending block.
fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd remove that method now, it's really easy to use transact instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed it. 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perfect, thanks!

@dvdplm dvdplm merged commit 2b1d148 into openethereum:master Dec 17, 2019
@afck afck deleted the afck-rand branch December 17, 2019 12:35
dvdplm added a commit that referenced this pull request Dec 19, 2019
* master:
  Add Nat PMP method to P2P module (#11210)
  Add randomness contract support to AuthorityRound. (#10946)
  ethcore/res: activate ecip-1061 on kotti and mordor (#11338)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A0-pleasereview 🤓 Pull request needs code review. F8-enhancement 🎊 An additional feature request. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants