use derive_more::Display;
use vapabi::Hash;
use vapabi_contract::use_contract;
use vapory_types::{Address, H256, U256};
use tetsy_keccak_hash::keccak;
use log::{debug, error};
use tetsy_crypto::publickey::{ecies, Error as CryptoError};
use tetsy_bytes::Bytes;
use rand::Rng;
use enjen::signer::EngineSigner;
use crate::util::{BoundContract, CallError};
pub type RandNumber = H256;
use_contract!(aura_random, "res/contracts/authority_round_random.json");
#[derive(Debug)]
pub enum RandomnessPhase {
Waiting,
BeforeCommit,
Committed,
Reveal { our_address: Address, round: U256 },
}
#[derive(Debug, Display)]
pub enum PhaseError {
#[display(fmt = "Revealed during commit phase")]
RevealedInCommit,
#[display(fmt = "Error loading randomness contract information: {:?}", _0)]
LoadFailed(CallError),
#[display(fmt = "Failed to load random number from the randomness contract")]
BadRandNumber,
#[display(fmt = "Failed to encrypt random number: {}", _0)]
Crypto(CryptoError),
#[display(fmt = "Failed to get the engine signer's public key")]
MissingPublicKey,
}
impl From<CryptoError> for PhaseError {
fn from(err: CryptoError) -> PhaseError {
PhaseError::Crypto(err)
}
}
impl RandomnessPhase {
pub fn load(
contract: &BoundContract,
our_address: Address,
) -> Result<RandomnessPhase, PhaseError> {
let round = contract
.call_const(aura_random::functions::current_collect_round::call())
.map_err(PhaseError::LoadFailed)?;
let is_commit_phase = contract
.call_const(aura_random::functions::is_commit_phase::call())
.map_err(PhaseError::LoadFailed)?;
let committed = contract
.call_const(aura_random::functions::is_committed::call(
round,
our_address,
))
.map_err(PhaseError::LoadFailed)?;
let revealed: bool = contract
.call_const(aura_random::functions::sent_reveal::call(
round,
our_address,
))
.map_err(PhaseError::LoadFailed)?;
if is_commit_phase {
if revealed {
return Err(PhaseError::RevealedInCommit);
}
if !committed {
Ok(RandomnessPhase::BeforeCommit)
} else {
Ok(RandomnessPhase::Committed)
}
} else {
if !committed {
return Ok(RandomnessPhase::Waiting);
}
if !revealed {
Ok(RandomnessPhase::Reveal { our_address, round })
} else {
Ok(RandomnessPhase::Waiting)
}
}
}
pub fn advance<R: Rng>(
self,
contract: &BoundContract,
rng: &mut R,
signer: &dyn EngineSigner,
) -> Result<Option<Bytes>, PhaseError> {
match self {
RandomnessPhase::Waiting | RandomnessPhase::Committed => Ok(None),
RandomnessPhase::BeforeCommit => {
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())?;
debug!(target: "engine", "Randomness contract: committing {}.", number_hash);
let (data, _decoder) = aura_random::functions::commit_hash::call(number_hash, cipher);
Ok(Some(data))
}
RandomnessPhase::Reveal { round, our_address } => {
let call = aura_random::functions::get_commit_and_cipher::call(round, our_address);
let (committed_hash, cipher) = contract
.call_const(call)
.map_err(PhaseError::LoadFailed)?;
let number_bytes = signer.decrypt(&committed_hash.0, &cipher)?;
let number = if number_bytes.len() == 32 {
RandNumber::from_slice(&number_bytes)
} else {
error!(target: "engine", "Decrypted random number has the wrong length.");
return Err(PhaseError::BadRandNumber);
};
let number_hash: Hash = keccak(number.as_bytes());
if number_hash != committed_hash {
error!(target: "engine", "Decrypted random number doesn't agree with the hash.");
return Err(PhaseError::BadRandNumber);
}
debug!(target: "engine", "Randomness contract: scheduling tx to reveal our random number {} (round={}, our_address={}).", number_hash, round, our_address);
let (data, _decoder) = aura_random::functions::reveal_number::call(number.as_bytes());
Ok(Some(data))
}
}
}
}