Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", tag = "v1

# revm
revm = { version = "34.0.0", default-features = false }
revm-bytecode = { version = "8.0.0", default-features = false }
revm-context-interface = { version = "14.0.0", default-features = false }
revm-database = { version = "10.0.0", default-features = false }
revm-inspector = { version = "15.0.0", default-features = false }
Expand Down
37 changes: 34 additions & 3 deletions contracts/scripts/ProtocolConfigManagement.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,6 @@ contract ProtocolConfigManagement is Script {
* @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig
*/
function updateRewardBeneficiary(address newBeneficiary) public {
require(newBeneficiary != address(0), "beneficiary cannot be zero address");

uint256 controllerKey = _getControllerKey();

vm.startBroadcast(controllerKey);
Expand All @@ -394,4 +392,37 @@ contract ProtocolConfigManagement is Script {
}
}


/// @title ProtocolConfigState
/// @notice Preserved-state hash helper used by upgrade/rollback scripts under
/// `contracts/deployments/<date>-protocol-config-*/scripts/`.
///
/// Aggregates every field an upgrade/rollback must preserve byte-for-byte and returns a
/// single hash. Pre-boundary and post-boundary calls should produce equal hashes; any
/// divergence indicates a storage-slot collision, accidental overwrite, or layout drift
/// between old and new implementations.
///
/// Validity condition: valid only when the struct definitions returned by the getters
/// (`FeeParams`, `ConsensusParams`) are unchanged between old and new impl. A struct
/// layout change would make `abi.encode` produce different bytes for the same logical
/// state — when that happens, replace this helper with field-by-field comparison on
/// the surviving fields.
///
/// `pauser` / `paused` are intentionally excluded. Their ERC-7201 slot may move across
/// a given upgrade, and during the upgrade window the value can be read from different
/// slots pre-vs-post boundary. Those fields belong in explicit specific-value
/// assertions at the call sites, not in this hash.
library ProtocolConfigState {
function hash(address proxy) internal view returns (bytes32) {
IProtocolConfig.FeeParams memory fee = ProtocolConfig(proxy).feeParams();
IProtocolConfig.ConsensusParams memory cons = ProtocolConfig(proxy).consensusParams();
return keccak256(
abi.encode(
fee,
cons,
ProtocolConfig(proxy).rewardBeneficiary(),
ProtocolConfig(proxy).owner(),
ProtocolConfig(proxy).controller()
)
);
}
}
21 changes: 21 additions & 0 deletions contracts/test/scripts/ProtocolConfigManagement.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,25 @@ contract ProtocolConfigManagementTest is Test {
assertEq(_feeParams.alpha, 0);
assertEq(_feeParams.kRate, 0);
}

function test_UpdateRewardBeneficiary_Succeeds() public {
vm.setEnv("CONTROLLER_KEY", vm.toString(controllerPk));

address newBeneficiary = makeAddr("newBeneficiary");
script.updateRewardBeneficiary(newBeneficiary);

assertEq(protocolConfig.rewardBeneficiary(), newBeneficiary);
}

function test_UpdateRewardBeneficiary_AcceptsZeroAddress() public {
vm.setEnv("CONTROLLER_KEY", vm.toString(controllerPk));

// Seed a non-zero beneficiary first so we can observe the clear.
address seedBeneficiary = makeAddr("seedBeneficiary");
script.updateRewardBeneficiary(seedBeneficiary);
assertEq(protocolConfig.rewardBeneficiary(), seedBeneficiary);

script.updateRewardBeneficiary(address(0));
assertEq(protocolConfig.rewardBeneficiary(), address(0));
}
}
70 changes: 43 additions & 27 deletions crates/consensus-db/src/repositories/undecided_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,33 @@ use arc_consensus_types::block::ConsensusBlock;
pub trait UndecidedBlocksRepository {
type Error: std::error::Error + Send + Sync + 'static;

/// Get the undecided block for the given height, round, and block hash.
///
/// # Arguments
/// - `height`: The height to get the undecided block for.
/// - `round`: The round to get the undecided block for.
async fn get(
/// Gets the undecided block with the given height, round, and block hash.
/// Returns `None` if no such block exists.
async fn get_by_round_and_hash(
&self,
height: Height,
round: Round,
block_hash: BlockHash,
) -> Result<Option<ConsensusBlock>, Self::Error>;

/// Get the undecided block for the given height and block hash (ignoring round).
/// Returns the first undecided block found that matches the height and block hash.
///
/// # Arguments
/// - `height`: The height to get the undecided block for.
/// - `block_hash`: The block hash to get the undecided block for.
async fn get_first(
/// Gets all undecided blocks for the given height and round.
async fn get_by_round(
&self,
height: Height,
round: Round,
) -> Result<Vec<ConsensusBlock>, Self::Error>;

/// Gets the undecided block with the given height and block hash.
/// Scans across all rounds, returning the first match found.
/// Returns `None` if no such block exists.
async fn get_by_hash(
&self,
height: Height,
block_hash: BlockHash,
) -> Result<Option<ConsensusBlock>, Self::Error>;

/// Store the undecided block.
///
/// # Arguments
/// - `block`: The block to store.
async fn store(&self, block: ConsensusBlock) -> Result<(), Self::Error>;
/// Stores the undecided block.
async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error>;
}

impl<T> UndecidedBlocksRepository for &'_ T
Expand All @@ -60,32 +58,42 @@ where
{
type Error = T::Error;

async fn get(
async fn get_by_round_and_hash(
&self,
height: Height,
round: Round,
block_hash: BlockHash,
) -> Result<Option<ConsensusBlock>, Self::Error> {
(*self).get(height, round, block_hash).await
(*self)
.get_by_round_and_hash(height, round, block_hash)
.await
}

async fn get_first(
async fn get_by_round(
&self,
height: Height,
round: Round,
) -> Result<Vec<ConsensusBlock>, Self::Error> {
(*self).get_by_round(height, round).await
}

async fn get_by_hash(
&self,
height: Height,
block_hash: BlockHash,
) -> Result<Option<ConsensusBlock>, Self::Error> {
(*self).get_first(height, block_hash).await
(*self).get_by_hash(height, block_hash).await
}

async fn store(&self, block: ConsensusBlock) -> Result<(), Self::Error> {
(*self).store(block).await
async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error> {
(*self).store_undecided_block(block).await
}
}

impl UndecidedBlocksRepository for Store {
type Error = StoreError;

async fn get(
async fn get_by_round_and_hash(
&self,
height: Height,
round: Round,
Expand All @@ -94,7 +102,15 @@ impl UndecidedBlocksRepository for Store {
self.get_undecided_block(height, round, block_hash).await
}

async fn get_first(
async fn get_by_round(
&self,
height: Height,
round: Round,
) -> Result<Vec<ConsensusBlock>, Self::Error> {
self.get_undecided_blocks(height, round).await
}

async fn get_by_hash(
&self,
height: Height,
block_hash: BlockHash,
Expand All @@ -103,7 +119,7 @@ impl UndecidedBlocksRepository for Store {
.await
}

async fn store(&self, block: ConsensusBlock) -> Result<(), Self::Error> {
async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error> {
self.store_undecided_block(block).await
}
}
4 changes: 3 additions & 1 deletion crates/evm-node/src/rpc_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ where
}

fn batch<'a>(&self, req: Batch<'a>) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
// Pending-block filtering is intentionally skipped for batch requests.
// --rpc.pending-block=none (binary default) + network topology make this unexploitable.
let batch = req
.into_iter()
.map(
Expand Down Expand Up @@ -448,7 +450,7 @@ mod tests {
// -- batch requests --
//
// Pending-tx subscription/filter are blocked in batch.
// Pending block interception is NOT applied in batch; falls back to
// Pending block interception is intentionally NOT applied in batch; falls back to
// --rpc.pending-block=none.

#[tokio::test]
Expand Down
5 changes: 4 additions & 1 deletion crates/execution-e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ alloy-network.workspace = true
alloy-primitives.workspace = true
alloy-rpc-types-engine.workspace = true
alloy-rpc-types-eth.workspace = true
alloy-rpc-types-trace.workspace = true
alloy-sol-types.workspace = true
arc-evm-node = { path = "../evm-node" }
# Arc crates
arc-execution-config = { path = "../execution-config", features = ["test-utils"] }
Expand Down Expand Up @@ -47,15 +49,16 @@ reth-rpc-api.workspace = true
reth-rpc-server-types.workspace = true
reth-tasks.workspace = true
reth-transaction-pool.workspace = true
serde_json.workspace = true

# Async
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true

[dev-dependencies]
alloy-sol-types.workspace = true
arc-precompiles = { path = "../precompiles", features = ["test-utils"] }
reth-tracing.workspace = true
revm-bytecode.workspace = true
rstest.workspace = true

[lints]
Expand Down
Loading
Loading