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

feat(entropy): Limit number of hashes #1822

Merged
merged 13 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions apps/fortuna/src/chain/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use {
abi::RawLog,
contract::{
abigen,
ContractCall,
EthLogDecode,
},
core::types::Address,
Expand Down Expand Up @@ -72,17 +73,18 @@ abigen!(
"../../target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json"
);

pub type SignablePythContractInner<T> = PythRandom<
LegacyTxMiddleware<
GasOracleMiddleware<
NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
EthProviderOracle<Provider<T>>,
>,
pub type MiddlewaresWrapper<T> = LegacyTxMiddleware<
GasOracleMiddleware<
NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
EthProviderOracle<Provider<T>>,
>,
>;
pub type SignablePythContractInner<T> = PythRandom<MiddlewaresWrapper<T>>;
pub type SignablePythContract = SignablePythContractInner<Http>;
pub type InstrumentedSignablePythContract = SignablePythContractInner<TracedClient>;

pub type PythContractCall = ContractCall<MiddlewaresWrapper<TracedClient>, ()>;

pub type PythContract = PythRandom<Provider<Http>>;
pub type InstrumentedPythContract = PythRandom<Provider<TracedClient>>;

Expand Down
28 changes: 28 additions & 0 deletions apps/fortuna/src/command/setup_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ async fn setup_chain_provider(
.in_current_span()
.await?;

sync_max_num_hashes(
&contract,
&provider_info,
chain_config.max_num_hashes.unwrap_or(0),
)
.in_current_span()
.await?;

Ok(())
}

Expand Down Expand Up @@ -248,3 +256,23 @@ async fn sync_fee_manager(
}
Ok(())
}


async fn sync_max_num_hashes(
contract: &Arc<SignablePythContract>,
provider_info: &ProviderInfo,
max_num_hashes: u32,
) -> Result<()> {
if provider_info.max_num_hashes != max_num_hashes {
tracing::info!("Updating provider max num hashes to {:?}", max_num_hashes);
if let Some(receipt) = contract
.set_max_num_hashes(max_num_hashes)
.send()
.await?
.await?
{
tracing::info!("Updated provider max num hashes to : {:?}", receipt);
}
}
Ok(())
}
4 changes: 4 additions & 0 deletions apps/fortuna/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ pub struct EthereumConfig {

/// Historical commitments made by the provider.
pub commitments: Option<Vec<Commitment>>,

/// Maximum number of hashes to record in a request.
/// This should be set according to the maximum gas limit the provider supports for callbacks.
pub max_num_hashes: Option<u32>,
}


Expand Down
122 changes: 90 additions & 32 deletions apps/fortuna/src/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use {
ethereum::{
InstrumentedPythContract,
InstrumentedSignablePythContract,
PythContractCall,
},
reader::{
BlockNumber,
Expand Down Expand Up @@ -87,6 +88,10 @@ const TRACK_INTERVAL: Duration = Duration::from_secs(10);
const WITHDRAW_INTERVAL: Duration = Duration::from_secs(300);
/// Check whether we need to adjust the fee at this interval.
const ADJUST_FEE_INTERVAL: Duration = Duration::from_secs(30);
/// Check whether we need to manually update the commitments to reduce numHashes for future
/// requests and reduce the gas cost of the reveal.
const UPDATE_COMMITMENTS_INTERVAL: Duration = Duration::from_secs(30);
const UPDATE_COMMITMENTS_THRESHOLD_FACTOR: f64 = 0.95;
/// Rety last N blocks
const RETRY_PREVIOUS_BLOCKS: u64 = 100;

Expand Down Expand Up @@ -314,6 +319,8 @@ pub async fn run_keeper_threads(
.in_current_span(),
);

spawn(update_commitments_loop(contract.clone(), chain_state.clone()).in_current_span());


// Spawn a thread to track the provider info and the balance of the keeper
spawn(
Expand Down Expand Up @@ -960,28 +967,46 @@ pub async fn withdraw_fees_if_necessary(
if keeper_balance < min_balance && U256::from(fees) > min_balance {
tracing::info!("Claiming accrued fees...");
let contract_call = contract.withdraw_as_fee_manager(provider_address, fees);
let pending_tx = contract_call
.send()
.await
.map_err(|e| anyhow!("Error submitting the withdrawal transaction: {:?}", e))?;

let tx_result = pending_tx
.await
.map_err(|e| anyhow!("Error waiting for withdrawal transaction receipt: {:?}", e))?
.ok_or_else(|| anyhow!("Can't verify the withdrawal, probably dropped from mempool"))?;

tracing::info!(
transaction_hash = &tx_result.transaction_hash.to_string(),
"Withdrew fees to keeper address. Receipt: {:?}",
tx_result,
);
send_and_confirm(contract_call).await?;
} else if keeper_balance < min_balance {
tracing::warn!("Keeper balance {:?} is too low (< {:?}) but provider fees are not sufficient to top-up.", keeper_balance, min_balance)
}

Ok(())
}

pub async fn send_and_confirm(contract_call: PythContractCall) -> Result<()> {
let call_name = contract_call.function.name.as_str();
let pending_tx = contract_call
.send()
.await
.map_err(|e| anyhow!("Error submitting transaction({}) {:?}", call_name, e))?;

let tx_result = pending_tx
.await
.map_err(|e| {
anyhow!(
"Error waiting for transaction({}) receipt: {:?}",
call_name,
e
)
})?
.ok_or_else(|| {
anyhow!(
"Can't verify the transaction({}), probably dropped from mempool",
call_name
)
})?;

tracing::info!(
transaction_hash = &tx_result.transaction_hash.to_string(),
"Confirmed transaction({}). Receipt: {:?}",
call_name,
tx_result,
);
Ok(())
}

#[tracing::instrument(name = "adjust_fee", skip_all)]
pub async fn adjust_fee_wrapper(
contract: Arc<InstrumentedSignablePythContract>,
Expand Down Expand Up @@ -1020,6 +1045,55 @@ pub async fn adjust_fee_wrapper(
}
}

#[tracing::instrument(name = "update_commitments", skip_all)]
pub async fn update_commitments_loop(
contract: Arc<InstrumentedSignablePythContract>,
chain_state: BlockchainState,
) {
loop {
if let Err(e) = update_commitments_if_necessary(contract.clone(), &chain_state)
.in_current_span()
.await
{
tracing::error!("Update commitments. error: {:?}", e);
}
time::sleep(UPDATE_COMMITMENTS_INTERVAL).await;
}
}


pub async fn update_commitments_if_necessary(
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: I think the verb "update" is a bit confusing here because it suggests that the caller could set the commitment to anything they want. A better name would be "advance", which implies that you can only move forward in the same hash chain.

contract: Arc<InstrumentedSignablePythContract>,
chain_state: &BlockchainState,
) -> Result<()> {
//TODO: we can reuse the result from the last call from the watch_blocks thread to reduce RPCs
let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
let provider_address = chain_state.provider_address;
let provider_info = contract
.get_provider_info(provider_address)
.block(latest_safe_block) // To ensure we are not revealing sooner than we should
.call()
.await
.map_err(|e| anyhow!("Error while getting provider info. error: {:?}", e))?;
if provider_info.max_num_hashes == 0 {
return Ok(());
}
let threshold =
((provider_info.max_num_hashes as f64) * UPDATE_COMMITMENTS_THRESHOLD_FACTOR) as u64;
if provider_info.sequence_number - provider_info.current_commitment_sequence_number > threshold
{
let seq_number = provider_info.sequence_number - 1;
let provider_revelation = chain_state
.state
.reveal(seq_number)
.map_err(|e| anyhow!("Error revealing: {:?}", e))?;
let contract_call =
contract.advance_provider_commitment(provider_address, seq_number, provider_revelation);
send_and_confirm(contract_call).await?;
}
Ok(())
}

/// Adjust the fee charged by the provider to ensure that it is profitable at the prevailing gas price.
/// This method targets a fee as a function of the maximum cost of the callback,
/// c = (gas_limit) * (current gas price), with min_fee_wei as a lower bound on the fee.
Expand Down Expand Up @@ -1105,23 +1179,7 @@ pub async fn adjust_fee_if_necessary(
target_fee
);
let contract_call = contract.set_provider_fee_as_fee_manager(provider_address, target_fee);
let pending_tx = contract_call
.send()
.await
.map_err(|e| anyhow!("Error submitting the set fee transaction: {:?}", e))?;

let tx_result = pending_tx
.await
.map_err(|e| anyhow!("Error waiting for set fee transaction receipt: {:?}", e))?
.ok_or_else(|| {
anyhow!("Can't verify the set fee transaction, probably dropped from mempool")
})?;

tracing::info!(
transaction_hash = &tx_result.transaction_hash.to_string(),
"Set provider fee. Receipt: {:?}",
tx_result,
);
send_and_confirm(contract_call).await?;

*sequence_number_of_last_fee_update = Some(provider_info.sequence_number);
} else {
Expand Down
69 changes: 69 additions & 0 deletions target_chains/ethereum/contracts/contracts/entropy/Entropy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ abstract contract Entropy is IEntropy, EntropyState {
assignedSequenceNumber -
providerInfo.currentCommitmentSequenceNumber
);
if (
providerInfo.maxNumHashes != 0 &&
req.numHashes > providerInfo.maxNumHashes
) {
revert EntropyErrors.LastRevealedTooOld();
}
req.commitment = keccak256(
bytes.concat(userCommitment, providerInfo.currentCommitment)
);
Expand Down Expand Up @@ -351,6 +357,50 @@ abstract contract Entropy is IEntropy, EntropyState {
}
}

// Advance the provider commitment and increase the sequence number.
// This is used to reduce the `numHashes` required for future requests which leads to reduced gas usage.
function advanceProviderCommitment(
address provider,
uint64 advancedSequenceNumber,
bytes32 providerRevelation
) public override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (
advancedSequenceNumber <=
providerInfo.currentCommitmentSequenceNumber
) revert EntropyErrors.UpdateTooOld();
if (advancedSequenceNumber >= providerInfo.endSequenceNumber)
revert EntropyErrors.AssertionFailure();

uint32 numHashes = SafeCast.toUint32(
advancedSequenceNumber -
providerInfo.currentCommitmentSequenceNumber
);
bytes32 providerCommitment = constructProviderCommitment(
numHashes,
providerRevelation
);

if (providerCommitment != providerInfo.currentCommitment)
revert EntropyErrors.IncorrectRevelation();

providerInfo.currentCommitmentSequenceNumber = advancedSequenceNumber;
providerInfo.currentCommitment = providerRevelation;
if (
providerInfo.currentCommitmentSequenceNumber >=
providerInfo.sequenceNumber
) {
// This means the provider called the function with a sequence number that was not yet requested.
// Assuming this is landed on-chain it's better to bump the sequence number and never use that range
// for future requests. Otherwise, someone can use the leaked revelation to derive favorable random numbers.
providerInfo.sequenceNumber =
providerInfo.currentCommitmentSequenceNumber +
1;
}
}

// Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
// against the corresponding commitments in the in-flight request. If both values are validated, this function returns
// the corresponding random number.
Expand Down Expand Up @@ -555,6 +605,25 @@ abstract contract Entropy is IEntropy, EntropyState {
emit ProviderFeeManagerUpdated(msg.sender, oldFeeManager, manager);
}

// Set the maximum number of hashes to record in a request. This should be set according to the maximum gas limit
// the provider supports for callbacks.
function setMaxNumHashes(uint32 maxNumHashes) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}

uint64 oldMaxNumHashes = provider.maxNumHashes;
provider.maxNumHashes = maxNumHashes;
emit ProviderMaxNumHashesUpdated(
msg.sender,
oldMaxNumHashes,
maxNumHashes
);
}

function constructUserCommitment(
bytes32 userRandomness
) public pure override returns (bytes32 userCommitment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@ contract EntropyUpgradable is
}

function version() public pure returns (string memory) {
return "0.3.1";
return "0.3.2";
}
}
Loading
Loading