Skip to content

Commit 8966444

Browse files
authored
feat(entropy): Limit number of hashes (#1822)
* Add revert reasons to Entropy tests * Revert if numHashes > maxNumHashes * Add update commitment function * Set maximum number of hashes in contract + keeper
1 parent 3205588 commit 8966444

File tree

16 files changed

+547
-73
lines changed

16 files changed

+547
-73
lines changed

apps/fortuna/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/fortuna/Cargo.toml

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

66
[dependencies]

apps/fortuna/src/chain/ethereum.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use {
2828
abi::RawLog,
2929
contract::{
3030
abigen,
31+
ContractCall,
3132
EthLogDecode,
3233
},
3334
core::types::Address,
@@ -72,17 +73,18 @@ abigen!(
7273
"../../target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json"
7374
);
7475

75-
pub type SignablePythContractInner<T> = PythRandom<
76-
LegacyTxMiddleware<
77-
GasOracleMiddleware<
78-
NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
79-
EthProviderOracle<Provider<T>>,
80-
>,
76+
pub type MiddlewaresWrapper<T> = LegacyTxMiddleware<
77+
GasOracleMiddleware<
78+
NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
79+
EthProviderOracle<Provider<T>>,
8180
>,
8281
>;
82+
pub type SignablePythContractInner<T> = PythRandom<MiddlewaresWrapper<T>>;
8383
pub type SignablePythContract = SignablePythContractInner<Http>;
8484
pub type InstrumentedSignablePythContract = SignablePythContractInner<TracedClient>;
8585

86+
pub type PythContractCall = ContractCall<MiddlewaresWrapper<TracedClient>, ()>;
87+
8688
pub type PythContract = PythRandom<Provider<Http>>;
8789
pub type InstrumentedPythContract = PythRandom<Provider<TracedClient>>;
8890

apps/fortuna/src/command/setup_provider.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ async fn setup_chain_provider(
193193
.in_current_span()
194194
.await?;
195195

196+
sync_max_num_hashes(
197+
&contract,
198+
&provider_info,
199+
chain_config.max_num_hashes.unwrap_or(0),
200+
)
201+
.in_current_span()
202+
.await?;
203+
196204
Ok(())
197205
}
198206

@@ -248,3 +256,23 @@ async fn sync_fee_manager(
248256
}
249257
Ok(())
250258
}
259+
260+
261+
async fn sync_max_num_hashes(
262+
contract: &Arc<SignablePythContract>,
263+
provider_info: &ProviderInfo,
264+
max_num_hashes: u32,
265+
) -> Result<()> {
266+
if provider_info.max_num_hashes != max_num_hashes {
267+
tracing::info!("Updating provider max num hashes to {:?}", max_num_hashes);
268+
if let Some(receipt) = contract
269+
.set_max_num_hashes(max_num_hashes)
270+
.send()
271+
.await?
272+
.await?
273+
{
274+
tracing::info!("Updated provider max num hashes to : {:?}", receipt);
275+
}
276+
}
277+
Ok(())
278+
}

apps/fortuna/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ pub struct EthereumConfig {
183183

184184
/// Historical commitments made by the provider.
185185
pub commitments: Option<Vec<Commitment>>,
186+
187+
/// Maximum number of hashes to record in a request.
188+
/// This should be set according to the maximum gas limit the provider supports for callbacks.
189+
pub max_num_hashes: Option<u32>,
186190
}
187191

188192

apps/fortuna/src/keeper.rs

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
ethereum::{
1111
InstrumentedPythContract,
1212
InstrumentedSignablePythContract,
13+
PythContractCall,
1314
},
1415
reader::{
1516
BlockNumber,
@@ -87,6 +88,10 @@ const TRACK_INTERVAL: Duration = Duration::from_secs(10);
8788
const WITHDRAW_INTERVAL: Duration = Duration::from_secs(300);
8889
/// Check whether we need to adjust the fee at this interval.
8990
const ADJUST_FEE_INTERVAL: Duration = Duration::from_secs(30);
91+
/// Check whether we need to manually update the commitments to reduce numHashes for future
92+
/// requests and reduce the gas cost of the reveal.
93+
const UPDATE_COMMITMENTS_INTERVAL: Duration = Duration::from_secs(30);
94+
const UPDATE_COMMITMENTS_THRESHOLD_FACTOR: f64 = 0.95;
9095
/// Rety last N blocks
9196
const RETRY_PREVIOUS_BLOCKS: u64 = 100;
9297

@@ -314,6 +319,8 @@ pub async fn run_keeper_threads(
314319
.in_current_span(),
315320
);
316321

322+
spawn(update_commitments_loop(contract.clone(), chain_state.clone()).in_current_span());
323+
317324

318325
// Spawn a thread to track the provider info and the balance of the keeper
319326
spawn(
@@ -960,28 +967,46 @@ pub async fn withdraw_fees_if_necessary(
960967
if keeper_balance < min_balance && U256::from(fees) > min_balance {
961968
tracing::info!("Claiming accrued fees...");
962969
let contract_call = contract.withdraw_as_fee_manager(provider_address, fees);
963-
let pending_tx = contract_call
964-
.send()
965-
.await
966-
.map_err(|e| anyhow!("Error submitting the withdrawal transaction: {:?}", e))?;
967-
968-
let tx_result = pending_tx
969-
.await
970-
.map_err(|e| anyhow!("Error waiting for withdrawal transaction receipt: {:?}", e))?
971-
.ok_or_else(|| anyhow!("Can't verify the withdrawal, probably dropped from mempool"))?;
972-
973-
tracing::info!(
974-
transaction_hash = &tx_result.transaction_hash.to_string(),
975-
"Withdrew fees to keeper address. Receipt: {:?}",
976-
tx_result,
977-
);
970+
send_and_confirm(contract_call).await?;
978971
} else if keeper_balance < min_balance {
979972
tracing::warn!("Keeper balance {:?} is too low (< {:?}) but provider fees are not sufficient to top-up.", keeper_balance, min_balance)
980973
}
981974

982975
Ok(())
983976
}
984977

978+
pub async fn send_and_confirm(contract_call: PythContractCall) -> Result<()> {
979+
let call_name = contract_call.function.name.as_str();
980+
let pending_tx = contract_call
981+
.send()
982+
.await
983+
.map_err(|e| anyhow!("Error submitting transaction({}) {:?}", call_name, e))?;
984+
985+
let tx_result = pending_tx
986+
.await
987+
.map_err(|e| {
988+
anyhow!(
989+
"Error waiting for transaction({}) receipt: {:?}",
990+
call_name,
991+
e
992+
)
993+
})?
994+
.ok_or_else(|| {
995+
anyhow!(
996+
"Can't verify the transaction({}), probably dropped from mempool",
997+
call_name
998+
)
999+
})?;
1000+
1001+
tracing::info!(
1002+
transaction_hash = &tx_result.transaction_hash.to_string(),
1003+
"Confirmed transaction({}). Receipt: {:?}",
1004+
call_name,
1005+
tx_result,
1006+
);
1007+
Ok(())
1008+
}
1009+
9851010
#[tracing::instrument(name = "adjust_fee", skip_all)]
9861011
pub async fn adjust_fee_wrapper(
9871012
contract: Arc<InstrumentedSignablePythContract>,
@@ -1020,6 +1045,55 @@ pub async fn adjust_fee_wrapper(
10201045
}
10211046
}
10221047

1048+
#[tracing::instrument(name = "update_commitments", skip_all)]
1049+
pub async fn update_commitments_loop(
1050+
contract: Arc<InstrumentedSignablePythContract>,
1051+
chain_state: BlockchainState,
1052+
) {
1053+
loop {
1054+
if let Err(e) = update_commitments_if_necessary(contract.clone(), &chain_state)
1055+
.in_current_span()
1056+
.await
1057+
{
1058+
tracing::error!("Update commitments. error: {:?}", e);
1059+
}
1060+
time::sleep(UPDATE_COMMITMENTS_INTERVAL).await;
1061+
}
1062+
}
1063+
1064+
1065+
pub async fn update_commitments_if_necessary(
1066+
contract: Arc<InstrumentedSignablePythContract>,
1067+
chain_state: &BlockchainState,
1068+
) -> Result<()> {
1069+
//TODO: we can reuse the result from the last call from the watch_blocks thread to reduce RPCs
1070+
let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
1071+
let provider_address = chain_state.provider_address;
1072+
let provider_info = contract
1073+
.get_provider_info(provider_address)
1074+
.block(latest_safe_block) // To ensure we are not revealing sooner than we should
1075+
.call()
1076+
.await
1077+
.map_err(|e| anyhow!("Error while getting provider info. error: {:?}", e))?;
1078+
if provider_info.max_num_hashes == 0 {
1079+
return Ok(());
1080+
}
1081+
let threshold =
1082+
((provider_info.max_num_hashes as f64) * UPDATE_COMMITMENTS_THRESHOLD_FACTOR) as u64;
1083+
if provider_info.sequence_number - provider_info.current_commitment_sequence_number > threshold
1084+
{
1085+
let seq_number = provider_info.sequence_number - 1;
1086+
let provider_revelation = chain_state
1087+
.state
1088+
.reveal(seq_number)
1089+
.map_err(|e| anyhow!("Error revealing: {:?}", e))?;
1090+
let contract_call =
1091+
contract.advance_provider_commitment(provider_address, seq_number, provider_revelation);
1092+
send_and_confirm(contract_call).await?;
1093+
}
1094+
Ok(())
1095+
}
1096+
10231097
/// Adjust the fee charged by the provider to ensure that it is profitable at the prevailing gas price.
10241098
/// This method targets a fee as a function of the maximum cost of the callback,
10251099
/// c = (gas_limit) * (current gas price), with min_fee_wei as a lower bound on the fee.
@@ -1105,23 +1179,7 @@ pub async fn adjust_fee_if_necessary(
11051179
target_fee
11061180
);
11071181
let contract_call = contract.set_provider_fee_as_fee_manager(provider_address, target_fee);
1108-
let pending_tx = contract_call
1109-
.send()
1110-
.await
1111-
.map_err(|e| anyhow!("Error submitting the set fee transaction: {:?}", e))?;
1112-
1113-
let tx_result = pending_tx
1114-
.await
1115-
.map_err(|e| anyhow!("Error waiting for set fee transaction receipt: {:?}", e))?
1116-
.ok_or_else(|| {
1117-
anyhow!("Can't verify the set fee transaction, probably dropped from mempool")
1118-
})?;
1119-
1120-
tracing::info!(
1121-
transaction_hash = &tx_result.transaction_hash.to_string(),
1122-
"Set provider fee. Receipt: {:?}",
1123-
tx_result,
1124-
);
1182+
send_and_confirm(contract_call).await?;
11251183

11261184
*sequence_number_of_last_fee_update = Some(provider_info.sequence_number);
11271185
} else {

target_chains/ethereum/contracts/contracts/entropy/Entropy.sol

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ abstract contract Entropy is IEntropy, EntropyState {
234234
assignedSequenceNumber -
235235
providerInfo.currentCommitmentSequenceNumber
236236
);
237+
if (
238+
providerInfo.maxNumHashes != 0 &&
239+
req.numHashes > providerInfo.maxNumHashes
240+
) {
241+
revert EntropyErrors.LastRevealedTooOld();
242+
}
237243
req.commitment = keccak256(
238244
bytes.concat(userCommitment, providerInfo.currentCommitment)
239245
);
@@ -351,6 +357,51 @@ abstract contract Entropy is IEntropy, EntropyState {
351357
}
352358
}
353359

360+
// Advance the provider commitment and increase the sequence number.
361+
// This is used to reduce the `numHashes` required for future requests which leads to reduced gas usage.
362+
function advanceProviderCommitment(
363+
address provider,
364+
uint64 advancedSequenceNumber,
365+
bytes32 providerRevelation
366+
) public override {
367+
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
368+
provider
369+
];
370+
if (
371+
advancedSequenceNumber <=
372+
providerInfo.currentCommitmentSequenceNumber
373+
) revert EntropyErrors.UpdateTooOld();
374+
if (advancedSequenceNumber >= providerInfo.endSequenceNumber)
375+
revert EntropyErrors.AssertionFailure();
376+
377+
uint32 numHashes = SafeCast.toUint32(
378+
advancedSequenceNumber -
379+
providerInfo.currentCommitmentSequenceNumber
380+
);
381+
bytes32 providerCommitment = constructProviderCommitment(
382+
numHashes,
383+
providerRevelation
384+
);
385+
386+
if (providerCommitment != providerInfo.currentCommitment)
387+
revert EntropyErrors.IncorrectRevelation();
388+
389+
providerInfo.currentCommitmentSequenceNumber = advancedSequenceNumber;
390+
providerInfo.currentCommitment = providerRevelation;
391+
if (
392+
providerInfo.currentCommitmentSequenceNumber >=
393+
providerInfo.sequenceNumber
394+
) {
395+
// This means the provider called the function with a sequence number that was not yet requested.
396+
// Providers should never do this and we consider such an implementation flawed.
397+
// Assuming this is landed on-chain it's better to bump the sequence number and never use that range
398+
// for future requests. Otherwise, someone can use the leaked revelation to derive favorable random numbers.
399+
providerInfo.sequenceNumber =
400+
providerInfo.currentCommitmentSequenceNumber +
401+
1;
402+
}
403+
}
404+
354405
// Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
355406
// against the corresponding commitments in the in-flight request. If both values are validated, this function returns
356407
// the corresponding random number.
@@ -555,6 +606,25 @@ abstract contract Entropy is IEntropy, EntropyState {
555606
emit ProviderFeeManagerUpdated(msg.sender, oldFeeManager, manager);
556607
}
557608

609+
// Set the maximum number of hashes to record in a request. This should be set according to the maximum gas limit
610+
// the provider supports for callbacks.
611+
function setMaxNumHashes(uint32 maxNumHashes) external override {
612+
EntropyStructs.ProviderInfo storage provider = _state.providers[
613+
msg.sender
614+
];
615+
if (provider.sequenceNumber == 0) {
616+
revert EntropyErrors.NoSuchProvider();
617+
}
618+
619+
uint32 oldMaxNumHashes = provider.maxNumHashes;
620+
provider.maxNumHashes = maxNumHashes;
621+
emit ProviderMaxNumHashesAdvanced(
622+
msg.sender,
623+
oldMaxNumHashes,
624+
maxNumHashes
625+
);
626+
}
627+
558628
function constructUserCommitment(
559629
bytes32 userRandomness
560630
) public pure override returns (bytes32 userCommitment) {

target_chains/ethereum/contracts/contracts/entropy/EntropyUpgradable.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,6 @@ contract EntropyUpgradable is
105105
}
106106

107107
function version() public pure returns (string memory) {
108-
return "0.3.1";
108+
return "0.4.0";
109109
}
110110
}

0 commit comments

Comments
 (0)