Skip to content

Commit d5cd12e

Browse files
authored
Merge pull request #39 from firstbatchxyz/erhant/handle-tx-underpriced
feat: handle tx underpriced
2 parents 188bfd4 + 3b7dc49 commit d5cd12e

File tree

12 files changed

+498
-432
lines changed

12 files changed

+498
-432
lines changed

Cargo.lock

Lines changed: 351 additions & 338 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ default-members = ["core"]
55

66
[workspace.package]
77
edition = "2021"
8-
version = "0.2.32"
8+
version = "0.2.33"
99
license = "Apache-2.0"
1010
readme = "README.md"
1111
authors = ["erhant"]

contracts/src/errors.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,24 @@ pub fn contract_error_report(error: Error) -> ErrReport {
3030
// here we try to parse the error w.r.t provided contract interfaces
3131
// or return a default one in the end if it was not parsed successfully
3232
if let Some(payload) = error.as_error_resp() {
33-
payload
34-
.as_decoded_error(false)
35-
.map(ERC20Errors::into)
36-
.or_else(|| {
37-
payload
38-
.as_decoded_error(false)
39-
.map(OracleRegistryErrors::into)
40-
})
41-
.or_else(|| {
42-
payload
43-
.as_decoded_error(false)
44-
.map(OracleCoordinatorErrors::into)
45-
})
46-
.unwrap_or(eyre!("Unhandled contract error: {:#?}", error))
33+
// an ERC20 error
34+
if let Some(erc_20_error) = payload.as_decoded_error::<ERC20Errors>(false) {
35+
return erc_20_error.into();
36+
} else
37+
// an OracleRegistry error
38+
if let Some(registry_error) =
39+
payload.as_decoded_error::<OracleRegistryErrors>(false)
40+
{
41+
return registry_error.into();
42+
} else
43+
// an OracleCoordinator error
44+
if let Some(coordinator_error) =
45+
payload.as_decoded_error::<OracleCoordinatorErrors>(false)
46+
{
47+
return coordinator_error.into();
48+
} else {
49+
return eyre!("Unhandled error response: {:#?}", error);
50+
}
4751
} else {
4852
eyre!("Unknown transport error: {:#?}", error)
4953
}
@@ -103,13 +107,13 @@ impl From<OracleRegistryErrors> for ErrReport {
103107
}
104108
// generic
105109
OracleRegistryErrors::FailedCall(_) => {
106-
eyre!("Failed call",)
110+
eyre!("Failed call")
107111
}
108112
OracleRegistryErrors::ERC1967InvalidImplementation(e) => {
109113
eyre!("Invalid implementation: {}", e.implementation)
110114
}
111115
OracleRegistryErrors::UUPSUnauthorizedCallContext(_) => {
112-
eyre!("Unauthorized UUPS call context",)
116+
eyre!("Unauthorized UUPS call context")
113117
}
114118
OracleRegistryErrors::UUPSUnsupportedProxiableUUID(e) => {
115119
eyre!("Unsupported UUPS proxiable UUID: {}", e.slot)
@@ -124,7 +128,7 @@ impl From<OracleRegistryErrors> for ErrReport {
124128
eyre!("Address {} is empty", e.target)
125129
}
126130
OracleRegistryErrors::NotInitializing(_) => {
127-
eyre!("Not initializing",)
131+
eyre!("Not initializing")
128132
}
129133
}
130134
}
@@ -170,13 +174,13 @@ impl From<OracleCoordinatorErrors> for ErrReport {
170174
}
171175
// generic
172176
OracleCoordinatorErrors::FailedInnerCall(_) => {
173-
eyre!("Failed inner call",)
177+
eyre!("Failed inner call")
174178
}
175179
OracleCoordinatorErrors::ERC1967InvalidImplementation(e) => {
176180
eyre!("Invalid implementation: {}", e.implementation)
177181
}
178182
OracleCoordinatorErrors::UUPSUnauthorizedCallContext(_) => {
179-
eyre!("Unauthorized UUPS call context",)
183+
eyre!("Unauthorized UUPS call context")
180184
}
181185
OracleCoordinatorErrors::UUPSUnsupportedProxiableUUID(e) => {
182186
eyre!("Unsupported UUPS proxiable UUID: {}", e.slot)
@@ -191,7 +195,7 @@ impl From<OracleCoordinatorErrors> for ErrReport {
191195
eyre!("Address {} is empty", e.target)
192196
}
193197
OracleCoordinatorErrors::NotInitializing(_) => {
194-
eyre!("Not initializing",)
198+
eyre!("Not initializing")
195199
}
196200
}
197201
}

core/src/cli/commands/coordinator/serve.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@ impl DriaOracle {
1818

1919
let status = TaskStatus::try_from(request.status)?;
2020
match handle_request(self, status, task_id, request.protocol).await {
21-
Ok(Some(receipt)) => {
22-
log::info!(
23-
"Task {} processed successfully. (tx: {})",
24-
task_id,
25-
receipt.transaction_hash
26-
)
27-
}
21+
Ok(Some(_receipt)) => {}
2822
Ok(None) => {
2923
log::info!("Task {} ignored.", task_id)
3024
}

core/src/cli/commands/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub enum Commands {
3434
Serve {
3535
#[arg(help = "The oracle kinds to handle tasks as, if omitted will default to all registered kinds.", value_parser = parse_oracle_kind)]
3636
kinds: Vec<OracleKind>,
37-
#[arg(short, long = "model", help = "The models to serve.", required = true, value_parser = parse_model)]
37+
#[arg(short, long = "model", help = "The model(s) to serve.", required = true, value_parser = parse_model)]
3838
models: Vec<Model>,
3939
#[arg(
4040
long,
@@ -49,6 +49,7 @@ pub enum Commands {
4949
)]
5050
to: Option<BlockNumberOrTag>,
5151
#[arg(
52+
short,
5253
long,
5354
help = "Optional task id to serve specifically.",
5455
required = false
@@ -61,14 +62,14 @@ pub enum Commands {
6162
from: Option<BlockNumberOrTag>,
6263
#[arg(long, help = "Ending block number, defaults to 'latest'.", value_parser = parse_block_number_or_tag)]
6364
to: Option<BlockNumberOrTag>,
64-
#[arg(long, help = "Task id to view.")]
65+
#[arg(short, long, help = "Task id to view.")]
6566
task_id: Option<U256>,
6667
},
6768
/// Request a task.
6869
Request {
6970
#[arg(help = "The input to request a task with.", required = true)]
7071
input: String,
71-
#[arg(help = "The models to accept.", required = true, value_parser=parse_model)]
72+
#[arg(help = "The model(s) to accept.", required = true, value_parser=parse_model)]
7273
models: Vec<Model>,
7374
#[arg(long, help = "The difficulty of the task.", default_value_t = 2)]
7475
difficulty: u8,

core/src/cli/commands/registry.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl DriaOracle {
2020
}
2121

2222
// calculate the required approval for registration
23-
let stake = self.registry_stake_amount(kind).await?;
23+
let stake = self.get_registry_stake_amount(kind).await?;
2424
let allowance = self
2525
.allowance(self.address(), self.addresses.registry)
2626
.await?;

core/src/cli/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl Cli {
3838
}
3939

4040
pub fn read_tx_timeout() -> Result<u64> {
41-
let timeout = env::var("TX_TIMEOUT_SECS").unwrap_or("30".to_string());
41+
let timeout = env::var("TX_TIMEOUT_SECS").unwrap_or("100".to_string());
4242
timeout.parse().map_err(Into::into)
4343
}
4444
}

core/src/node/anvil.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ impl DriaOracle {
5757
.map_err(contract_error_report)
5858
.wrap_err("could not add to whitelist")?;
5959

60+
// TODO: use common command wait_for_tx
6061
log::info!("Hash: {:?}", tx.tx_hash());
6162
let receipt = tx
6263
.with_timeout(self.config.tx_timeout)

core/src/node/coordinator.rs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ use alloy::eips::BlockNumberOrTag;
55
use alloy::primitives::aliases::U40;
66
use alloy::primitives::{Bytes, U256};
77
use alloy::rpc::types::{Log, TransactionReceipt};
8+
use dria_oracle_contracts::string_to_bytes32;
89
use dria_oracle_contracts::LLMOracleTask::{TaskResponse, TaskValidation};
9-
use dria_oracle_contracts::{contract_error_report, string_to_bytes32};
10-
use eyre::{eyre, Context, Result};
10+
use eyre::{eyre, Result};
1111

1212
use dria_oracle_contracts::OracleCoordinator::{
1313
self, getFeeReturn, getResponsesReturn, getValidationsReturn, requestsReturn,
@@ -32,13 +32,9 @@ impl DriaOracle {
3232
numGenerations: U40::from(num_gens),
3333
numValidations: U40::from(num_vals),
3434
};
35-
let req = coordinator.request(string_to_bytes32(protocol)?, input, models, parameters);
36-
let tx = req
37-
.send()
38-
.await
39-
.map_err(contract_error_report)
40-
.wrap_err("could not request task")?;
4135

36+
let req = coordinator.request(string_to_bytes32(protocol)?, input, models, parameters);
37+
let tx = self.send_with_gas_hikes(req).await?;
4238
self.wait_for_tx(tx).await
4339
}
4440

@@ -78,6 +74,7 @@ impl DriaOracle {
7874
Ok(responses._0)
7975
}
8076

77+
/// Responds to a generation request with the response, metadata, and a valid nonce.
8178
pub async fn respond_generation(
8279
&self,
8380
task_id: U256,
@@ -88,16 +85,12 @@ impl DriaOracle {
8885
let coordinator = OracleCoordinator::new(self.addresses.coordinator, &self.provider);
8986

9087
let req = coordinator.respond(task_id, nonce, response, metadata);
91-
let tx = req.send().await.map_err(contract_error_report)?;
92-
93-
log::info!("Hash: {:?}", tx.tx_hash());
94-
let receipt = tx
95-
.with_timeout(self.config.tx_timeout)
96-
.get_receipt()
97-
.await?;
98-
Ok(receipt)
88+
let tx = self.send_with_gas_hikes(req).await?;
89+
self.wait_for_tx(tx).await
9990
}
10091

92+
/// Responds to a validation request with the score, metadata, and a valid nonce.
93+
#[inline]
10194
pub async fn respond_validation(
10295
&self,
10396
task_id: U256,
@@ -108,12 +101,12 @@ impl DriaOracle {
108101
let coordinator = OracleCoordinator::new(self.addresses.coordinator, &self.provider);
109102

110103
let req = coordinator.validate(task_id, nonce, scores, metadata);
111-
let tx = req.send().await.map_err(contract_error_report)?;
112-
104+
let tx = self.send_with_gas_hikes(req).await?;
113105
self.wait_for_tx(tx).await
114106
}
115107

116108
/// Subscribes to events & processes tasks.
109+
#[inline]
117110
pub async fn subscribe_to_tasks(
118111
&self,
119112
) -> Result<EventPoller<DriaOracleProviderTransport, StatusUpdate>> {
@@ -163,6 +156,7 @@ impl DriaOracle {
163156
}
164157

165158
/// Returns the next task id.
159+
#[inline]
166160
pub async fn get_next_task_id(&self) -> Result<U256> {
167161
let coordinator = OracleCoordinator::new(self.addresses.coordinator, &self.provider);
168162

core/src/node/mod.rs

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ mod token;
66
mod anvil;
77

88
use super::DriaOracleConfig;
9+
use alloy::contract::CallBuilder;
910
use alloy::hex::FromHex;
1011
use alloy::providers::fillers::{
1112
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
1213
};
1314
use alloy::providers::{PendingTransactionBuilder, WalletProvider};
14-
use alloy::rpc::types::TransactionReceipt;
15+
use alloy::transports::RpcError;
1516
use alloy::{
1617
network::{Ethereum, EthereumWallet},
1718
primitives::Address,
@@ -21,8 +22,8 @@ use alloy::{
2122
use alloy_chains::Chain;
2223
use dkn_workflows::{DriaWorkflowsConfig, Model, ModelProvider};
2324
use dria_oracle_contracts::{
24-
get_coordinator_address, ContractAddresses, OracleCoordinator, OracleKind, OracleRegistry,
25-
TokenBalance,
25+
contract_error_report, get_coordinator_address, ContractAddresses, OracleCoordinator,
26+
OracleKind, OracleRegistry, TokenBalance,
2627
};
2728
use eyre::{eyre, Context, Result};
2829
use std::env;
@@ -259,17 +260,87 @@ impl DriaOracle {
259260
}
260261

261262
/// Waits for a transaction to be mined, returning the receipt.
262-
async fn wait_for_tx(
263+
#[inline]
264+
async fn wait_for_tx<T, N>(
263265
&self,
264-
tx: PendingTransactionBuilder<Http<Client>, Ethereum>,
265-
) -> Result<TransactionReceipt> {
266+
tx: PendingTransactionBuilder<T, N>,
267+
) -> Result<N::ReceiptResponse>
268+
where
269+
T: alloy::transports::Transport + Clone,
270+
N: alloy::network::Network,
271+
{
266272
log::info!("Waiting for tx: {:?}", tx.tx_hash());
267273
let receipt = tx
268274
.with_timeout(self.config.tx_timeout)
269275
.get_receipt()
270276
.await?;
271277
Ok(receipt)
272278
}
279+
280+
/// Given a request, retries sending it with increasing gas prices to avoid
281+
/// the "tx underpriced" errors.
282+
#[inline]
283+
async fn send_with_gas_hikes<T, P, D, N>(
284+
&self,
285+
req: CallBuilder<T, P, D, N>,
286+
) -> Result<PendingTransactionBuilder<T, N>>
287+
where
288+
T: alloy::transports::Transport + Clone,
289+
P: alloy::providers::Provider<T, N> + Clone,
290+
D: alloy::contract::CallDecoder + Clone,
291+
N: alloy::network::Network,
292+
{
293+
// gas price hikes to try in increasing order, first is 0 to simply use the
294+
// initial gas fee for the first attempt
295+
const GAS_PRICE_HIKES: [u128; 4] = [0, 12, 24, 36];
296+
297+
// try and send tx, with increasing gas prices for few attempts
298+
let initial_gas_price = self.provider.get_gas_price().await?;
299+
for (attempt_no, increase_percentage) in GAS_PRICE_HIKES.iter().enumerate() {
300+
// set gas price
301+
let gas_price = initial_gas_price + (initial_gas_price / 100) * increase_percentage;
302+
303+
// try to send tx with gas price
304+
match req
305+
.clone()
306+
.gas_price(gas_price) // TODO: very low gas price to get an error deliberately
307+
.send()
308+
.await
309+
{
310+
// if all is well, we can return the tx
311+
Ok(tx) => {
312+
return Ok(tx);
313+
}
314+
// if we get an RPC error; specifically, if the tx is underpriced, we try again with higher gas
315+
Err(alloy::contract::Error::TransportError(RpcError::ErrorResp(err))) => {
316+
// TODO: kind of a code-smell, can we do better check here?
317+
if err.message.contains("underpriced") {
318+
log::warn!(
319+
"{} with gas {} in attempt {}",
320+
err.message,
321+
gas_price,
322+
attempt_no + 1,
323+
);
324+
325+
// wait just a little bit
326+
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
327+
328+
continue;
329+
} else {
330+
// otherwise let it be handled by the error report
331+
return Err(contract_error_report(
332+
alloy::contract::Error::TransportError(RpcError::ErrorResp(err)),
333+
));
334+
}
335+
}
336+
// if we get any other error, we report it
337+
Err(err) => return Err(contract_error_report(err)),
338+
};
339+
}
340+
341+
// all attempts failed
342+
Err(eyre!("Failed to send transaction."))
343+
}
273344
}
274345

275346
impl core::fmt::Display for DriaOracle {

0 commit comments

Comments
 (0)