Skip to content

Commit 008e59d

Browse files
shunsukewdancoombs
andauthored
fix: EREP-015 rule Paymaster opsSeen decrement (#905)
Co-authored-by: Dan Coombs <[email protected]>
1 parent a92183b commit 008e59d

File tree

7 files changed

+330
-8
lines changed

7 files changed

+330
-8
lines changed

crates/builder/src/bundle_proposer.rs

Lines changed: 276 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ use rundler_types::{
4040
da::DAGasBlockData,
4141
pool::{Pool, PoolOperation, SimulationViolation},
4242
Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType, GasFees,
43-
Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, BUNDLE_BYTE_OVERHEAD,
44-
TIME_RANGE_BUFFER, USER_OP_OFFSET_WORD_SIZE,
43+
Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, ValidationRevert,
44+
BUNDLE_BYTE_OVERHEAD, TIME_RANGE_BUFFER, USER_OP_OFFSET_WORD_SIZE,
4545
};
4646
use rundler_utils::{emit::WithEntryPoint, math};
4747
use tokio::{sync::broadcast, try_join};
@@ -1280,6 +1280,7 @@ impl<UO: UserOperation> ProposalContext<UO> {
12801280
} else {
12811281
EntityUpdateType::UnstakedInvalidation
12821282
},
1283+
..Default::default()
12831284
},
12841285
);
12851286
ret
@@ -1371,33 +1372,48 @@ impl<UO: UserOperation> ProposalContext<UO> {
13711372
violations: Vec<SimulationViolation>,
13721373
entity_infos: EntityInfos,
13731374
) {
1375+
// If a staked factory or sender is present, we attribute errors to them directly.
1376+
1377+
// In accordance with [EREP-015]/[EREP-020]/[EREP-030], responsibility for failures lies with the staked factory or account.
1378+
// Paymasters should not be held accountable, so the paymaster's `opsSeen` count should be decremented accordingly.
1379+
let is_staked_factory = entity_infos.factory.map_or(false, |f| f.is_staked);
1380+
let is_staked_sender = entity_infos.sender.is_staked;
1381+
if is_staked_factory || is_staked_sender {
1382+
if let Some(paymaster) = entity_infos.paymaster {
1383+
self.add_erep_015_paymaster_amendment(paymaster.address())
1384+
}
1385+
}
1386+
13741387
// [EREP-020] When there is a staked factory any error in validation is attributed to it.
1375-
if entity_infos.factory.map_or(false, |f| f.is_staked) {
1388+
if is_staked_factory {
13761389
let factory = entity_infos.factory.unwrap();
13771390
self.entity_updates.insert(
13781391
factory.address(),
13791392
EntityUpdate {
13801393
entity: factory.entity,
13811394
update_type: EntityUpdateType::StakedInvalidation,
1395+
value: None,
13821396
},
13831397
);
13841398
return;
13851399
}
13861400

13871401
// [EREP-030] When there is a staked sender (without a staked factory) any error in validation is attributed to it.
1388-
if entity_infos.sender.is_staked {
1402+
if is_staked_sender {
13891403
self.entity_updates.insert(
13901404
entity_infos.sender.address(),
13911405
EntityUpdate {
13921406
entity: entity_infos.sender.entity,
13931407
update_type: EntityUpdateType::StakedInvalidation,
1408+
value: None,
13941409
},
13951410
);
13961411
return;
13971412
}
13981413

13991414
// If not a staked factory or sender, attribute errors to each entity directly.
14001415
// For a given op, there can only be a single update per entity so we don't double count the [UREP-030] throttle penalty.
1416+
let mut paymaster_amendment_required = false;
14011417
for violation in violations {
14021418
match violation {
14031419
SimulationViolation::UsedForbiddenOpcode(entity, _, _) => {
@@ -1450,19 +1466,34 @@ impl<UO: UserOperation> ProposalContext<UO> {
14501466
);
14511467
}
14521468
}
1469+
SimulationViolation::ValidationRevert(ValidationRevert::Operation {
1470+
entry_point_reason,
1471+
..
1472+
}) => {
1473+
// [EREP-015] If user operations error derived from factory or account, paymaster opsSeen amendment is required.
1474+
paymaster_amendment_required |=
1475+
matches!(&entry_point_reason[..3], "AA1" | "AA2");
1476+
}
14531477
SimulationViolation::OutOfGas(entity) => {
14541478
self.add_entity_update(entity, entity_infos)
14551479
}
14561480
_ => continue,
14571481
}
14581482
}
1483+
1484+
if paymaster_amendment_required {
1485+
if let Some(paymaster) = entity_infos.paymaster {
1486+
self.add_erep_015_paymaster_amendment(paymaster.address())
1487+
};
1488+
}
14591489
}
14601490

14611491
// Add an entity update for the entity
14621492
fn add_entity_update(&mut self, entity: Entity, entity_infos: EntityInfos) {
14631493
let entity_update = EntityUpdate {
14641494
entity,
14651495
update_type: ProposalContext::<UO>::get_entity_update_type(entity.kind, entity_infos),
1496+
value: None,
14661497
};
14671498
self.entity_updates.insert(entity.address, entity_update);
14681499
}
@@ -1517,6 +1548,25 @@ impl<UO: UserOperation> ProposalContext<UO> {
15171548
}
15181549
}
15191550
}
1551+
1552+
fn add_erep_015_paymaster_amendment(&mut self, address: Address) {
1553+
// Insert to entity_updates if entry is vacant, otherwise increment the value (precicely EntityUpdate.value)
1554+
self.entity_updates
1555+
.entry(address)
1556+
.and_modify(|e| {
1557+
if let Some(value) = &mut e.value {
1558+
*value += 1;
1559+
}
1560+
})
1561+
.or_insert_with(|| EntityUpdate {
1562+
entity: Entity {
1563+
kind: EntityType::Paymaster,
1564+
address,
1565+
},
1566+
update_type: EntityUpdateType::PaymasterOpsSeenDecrement,
1567+
value: Some(1),
1568+
});
1569+
}
15201570
}
15211571

15221572
#[cfg(test)]
@@ -1991,10 +2041,12 @@ mod tests {
19912041
EntityUpdate {
19922042
entity: Entity::paymaster(address(1)),
19932043
update_type: EntityUpdateType::UnstakedInvalidation,
2044+
..Default::default()
19942045
},
19952046
EntityUpdate {
19962047
entity: Entity::factory(address(3)),
19972048
update_type: EntityUpdateType::UnstakedInvalidation,
2049+
..Default::default()
19982050
},
19992051
]
20002052
);
@@ -2008,6 +2060,212 @@ mod tests {
20082060
);
20092061
}
20102062

2063+
#[tokio::test]
2064+
async fn test_paymaster_amended_by_staked_factory_revert() {
2065+
let sender = address(1);
2066+
let staked_factory = address(2);
2067+
let paymaster = address(3);
2068+
2069+
let deposit = parse_units("1", "ether").unwrap().into();
2070+
2071+
let entity_infos = EntityInfos {
2072+
sender: EntityInfo::new(Entity::account(sender), false),
2073+
factory: Some(EntityInfo::new(Entity::factory(staked_factory), true)),
2074+
paymaster: Some(EntityInfo::new(Entity::paymaster(paymaster), false)),
2075+
aggregator: None,
2076+
};
2077+
2078+
// EREP-015: If a staked factory or sender is present, we attribute errors to them directly.
2079+
// Expect EntityUpdateType::PaymasterOpsSeenDecrement to be recorded.
2080+
let op = op_with_sender_factory_paymaster(sender, staked_factory, paymaster);
2081+
let bundle = mock_make_bundle(
2082+
vec![MockOp {
2083+
op: op.clone(),
2084+
simulation_result: Box::new(move || {
2085+
Err(SimulationError {
2086+
violation_error: ViolationError::Violations(vec![]),
2087+
entity_infos: Some(entity_infos),
2088+
})
2089+
}),
2090+
}],
2091+
vec![],
2092+
vec![],
2093+
vec![deposit],
2094+
0,
2095+
0,
2096+
false,
2097+
ExpectedStorage::default(),
2098+
false,
2099+
)
2100+
.await;
2101+
2102+
let mut actual_entity_updates = bundle.entity_updates;
2103+
let mut expected_entity_updates = vec![
2104+
EntityUpdate {
2105+
entity: Entity::factory(staked_factory),
2106+
update_type: EntityUpdateType::StakedInvalidation,
2107+
..Default::default()
2108+
},
2109+
EntityUpdate {
2110+
entity: Entity::paymaster(paymaster),
2111+
update_type: EntityUpdateType::PaymasterOpsSeenDecrement,
2112+
value: Some(1),
2113+
},
2114+
];
2115+
2116+
// we want to check that the entity updates are the same regardless of order
2117+
actual_entity_updates.sort_by(|a, b| a.entity.address.cmp(&b.entity.address));
2118+
expected_entity_updates.sort_by(|a, b| a.entity.address.cmp(&b.entity.address));
2119+
2120+
assert_eq!(actual_entity_updates, expected_entity_updates);
2121+
}
2122+
2123+
#[tokio::test]
2124+
async fn test_paymaster_amended_by_staked_sender_revert() {
2125+
let sender = address(1);
2126+
let paymaster = address(2);
2127+
2128+
let deposit = parse_units("1", "ether").unwrap().into();
2129+
2130+
let entity_infos = EntityInfos {
2131+
sender: EntityInfo::new(Entity::account(sender), true),
2132+
factory: None,
2133+
paymaster: Some(EntityInfo::new(Entity::paymaster(paymaster), false)),
2134+
aggregator: None,
2135+
};
2136+
2137+
// EREP-015: If not a staked factory or sender, attribute errors to each entity directly.
2138+
// Expect EntityUpdateType::PaymasterOpsSeenDecrement to be recorded.
2139+
let op = op_with_sender_paymaster(sender, paymaster);
2140+
let bundle = mock_make_bundle(
2141+
vec![MockOp {
2142+
op: op.clone(),
2143+
simulation_result: Box::new(move || {
2144+
Err(SimulationError {
2145+
violation_error: ViolationError::Violations(vec![]),
2146+
entity_infos: Some(entity_infos),
2147+
})
2148+
}),
2149+
}],
2150+
vec![],
2151+
vec![],
2152+
vec![deposit],
2153+
0,
2154+
0,
2155+
false,
2156+
ExpectedStorage::default(),
2157+
false,
2158+
)
2159+
.await;
2160+
2161+
let mut actual_entity_updates = bundle.entity_updates;
2162+
let mut expected_entity_updates = vec![
2163+
EntityUpdate {
2164+
entity: Entity::account(sender),
2165+
update_type: EntityUpdateType::StakedInvalidation,
2166+
..Default::default()
2167+
},
2168+
EntityUpdate {
2169+
entity: Entity::paymaster(paymaster),
2170+
update_type: EntityUpdateType::PaymasterOpsSeenDecrement,
2171+
value: Some(1),
2172+
},
2173+
];
2174+
2175+
// we want to check that the entity updates are the same regardless of order
2176+
actual_entity_updates.sort_by(|a, b| a.entity.address.cmp(&b.entity.address));
2177+
expected_entity_updates.sort_by(|a, b| a.entity.address.cmp(&b.entity.address));
2178+
2179+
assert_eq!(actual_entity_updates, expected_entity_updates);
2180+
}
2181+
2182+
#[tokio::test]
2183+
async fn test_paymaster_amended_by_factory_or_sender_revert() {
2184+
let sender_1 = address(1);
2185+
let sender_2 = address(2);
2186+
let factory = address(3);
2187+
let paymaster = address(4);
2188+
2189+
let deposit = parse_units("1", "ether").unwrap().into();
2190+
2191+
let entity_infos_1 = EntityInfos {
2192+
sender: EntityInfo::new(Entity::account(sender_1), false),
2193+
factory: Some(EntityInfo::new(Entity::factory(factory), false)),
2194+
paymaster: Some(EntityInfo::new(Entity::paymaster(paymaster), false)),
2195+
aggregator: None,
2196+
};
2197+
2198+
let entity_infos_2 = EntityInfos {
2199+
sender: EntityInfo::new(Entity::account(sender_2), false),
2200+
factory: Some(EntityInfo::new(Entity::factory(factory), false)),
2201+
paymaster: Some(EntityInfo::new(Entity::paymaster(paymaster), false)),
2202+
aggregator: None,
2203+
};
2204+
2205+
// EREP-015: If a staked factory or sender is present, we attribute errors to them directly.
2206+
// Expect EntityUpdateType::PaymasterOpsSeenDecrement to be recorded.
2207+
let op_1 = op_with_sender_factory_paymaster(sender_1, factory, paymaster);
2208+
let op_2 = op_with_sender_factory_paymaster(sender_2, factory, paymaster);
2209+
let bundle = mock_make_bundle(
2210+
vec![
2211+
MockOp {
2212+
op: op_1.clone(),
2213+
simulation_result: Box::new(move || {
2214+
Err(SimulationError {
2215+
violation_error: ViolationError::Violations(vec![
2216+
SimulationViolation::ValidationRevert(
2217+
ValidationRevert::Operation {
2218+
entry_point_reason: "AA1x: factory related errors"
2219+
.to_string(),
2220+
inner_revert_reason: None,
2221+
inner_revert_data: Bytes::new(),
2222+
},
2223+
),
2224+
]),
2225+
entity_infos: Some(entity_infos_1),
2226+
})
2227+
}),
2228+
},
2229+
MockOp {
2230+
op: op_2.clone(),
2231+
simulation_result: Box::new(move || {
2232+
Err(SimulationError {
2233+
violation_error: ViolationError::Violations(vec![
2234+
SimulationViolation::ValidationRevert(
2235+
ValidationRevert::Operation {
2236+
entry_point_reason: "AA2x: sender related errors"
2237+
.to_string(),
2238+
inner_revert_reason: None,
2239+
inner_revert_data: Bytes::new(),
2240+
},
2241+
),
2242+
]),
2243+
entity_infos: Some(entity_infos_2),
2244+
})
2245+
}),
2246+
},
2247+
],
2248+
vec![],
2249+
vec![],
2250+
vec![deposit, deposit],
2251+
0,
2252+
0,
2253+
false,
2254+
ExpectedStorage::default(),
2255+
false,
2256+
)
2257+
.await;
2258+
2259+
let actual_entity_updates = bundle.entity_updates;
2260+
let expected_entity_updates = vec![EntityUpdate {
2261+
entity: Entity::paymaster(paymaster),
2262+
update_type: EntityUpdateType::PaymasterOpsSeenDecrement,
2263+
value: Some(2),
2264+
}];
2265+
2266+
assert_eq!(actual_entity_updates, expected_entity_updates);
2267+
}
2268+
20112269
#[tokio::test]
20122270
async fn test_bundle_gas_limit_simple() {
20132271
// Limit is 10M
@@ -2596,6 +2854,20 @@ mod tests {
25962854
}
25972855
}
25982856

2857+
fn op_with_sender_factory_paymaster(
2858+
sender: Address,
2859+
factory: Address,
2860+
paymaster: Address,
2861+
) -> UserOperation {
2862+
UserOperation {
2863+
sender,
2864+
init_code: factory.to_vec().into(),
2865+
paymaster_and_data: paymaster.to_vec().into(),
2866+
pre_verification_gas: DEFAULT_PVG,
2867+
..Default::default()
2868+
}
2869+
}
2870+
25992871
fn op_with_sender_and_fees(
26002872
sender: Address,
26012873
max_fee_per_gas: u128,

crates/pool/proto/op_pool/op_pool.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,14 @@ enum EntityUpdateType {
111111
ENTITY_UPDATE_TYPE_UNSPECIFIED = 0;
112112
ENTITY_UPDATE_TYPE_UNSTAKED_INVALIDATION = 1;
113113
ENTITY_UPDATE_TYPE_STAKED_INVALIDATION = 2;
114+
ENTITY_UPDATE_TYPE_PAYMASTER_OPS_SEEN_DECREMENT = 3;
114115
}
115116

116117
// A tuple consisting of an entity and what kind of update to perform on it
117118
message EntityUpdate {
118119
Entity entity = 1;
119120
EntityUpdateType update_type = 2;
121+
uint64 value = 3;
120122
}
121123

122124
// Defines a UserOperation persisted in a local mempool

0 commit comments

Comments
 (0)