Commit c47fe5d9 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into optional-governance

parents 8f7ac67a 34fae45a
This diff is collapsed.
...@@ -2,15 +2,20 @@ package bindings ...@@ -2,15 +2,20 @@ package bindings
import ( import (
"fmt" "fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-bindings/solc" "github.com/ethereum-optimism/optimism/op-bindings/solc"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
// layouts respresents the set of storage layouts. It is populated in an init function.
var layouts = make(map[string]*solc.StorageLayout) var layouts = make(map[string]*solc.StorageLayout)
// deployedBytecodes represents the set of deployed bytecodes. It is populated
// in an init function.
var deployedBytecodes = make(map[string]string) var deployedBytecodes = make(map[string]string)
// GetStorageLayout returns the storage layout of a contract by name.
func GetStorageLayout(name string) (*solc.StorageLayout, error) { func GetStorageLayout(name string) (*solc.StorageLayout, error) {
layout := layouts[name] layout := layouts[name]
if layout == nil { if layout == nil {
...@@ -19,11 +24,36 @@ func GetStorageLayout(name string) (*solc.StorageLayout, error) { ...@@ -19,11 +24,36 @@ func GetStorageLayout(name string) (*solc.StorageLayout, error) {
return layout, nil return layout, nil
} }
// GetDeployedBytecode returns the deployed bytecode of a contract by name.
func GetDeployedBytecode(name string) ([]byte, error) { func GetDeployedBytecode(name string) ([]byte, error) {
bc := deployedBytecodes[name] bc := deployedBytecodes[name]
if bc == "" { if bc == "" {
return nil, fmt.Errorf("%s: deployed bytecode not found", name) return nil, fmt.Errorf("%s: deployed bytecode not found", name)
} }
if !isHex(bc) {
return nil, fmt.Errorf("%s: invalid deployed bytecode", name)
}
return common.FromHex(bc), nil return common.FromHex(bc), nil
} }
// isHexCharacter returns bool of c being a valid hexadecimal.
func isHexCharacter(c byte) bool {
return ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')
}
// isHex validates whether each byte is valid hexadecimal string.
func isHex(str string) bool {
if len(str)%2 != 0 {
return false
}
str = strings.TrimPrefix(str, "0x")
for _, c := range []byte(str) {
if !isHexCharacter(c) {
return false
}
}
return true
}
...@@ -816,7 +816,7 @@ func TestSystemDenseTopology(t *testing.T) { ...@@ -816,7 +816,7 @@ func TestSystemDenseTopology(t *testing.T) {
params, err := p2p.GetPeerScoreParams("light", 2) params, err := p2p.GetPeerScoreParams("light", 2)
require.NoError(t, err) require.NoError(t, err)
node.P2P = &p2p.Config{ node.P2P = &p2p.Config{
PeerScoring: params, PeerScoring: &params,
BanningEnabled: false, BanningEnabled: false,
} }
} }
......
...@@ -98,7 +98,7 @@ func loadTopicScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64 ...@@ -98,7 +98,7 @@ func loadTopicScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64
if err != nil { if err != nil {
return err return err
} }
conf.TopicScoring = topicScoreParams conf.TopicScoring = &topicScoreParams
} }
return nil return nil
...@@ -114,7 +114,7 @@ func loadPeerScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64) ...@@ -114,7 +114,7 @@ func loadPeerScoringParams(conf *p2p.Config, ctx *cli.Context, blockTime uint64)
if err != nil { if err != nil {
return err return err
} }
conf.PeerScoring = peerScoreParams conf.PeerScoring = &peerScoreParams
} }
return nil return nil
......
...@@ -63,8 +63,8 @@ type Config struct { ...@@ -63,8 +63,8 @@ type Config struct {
AltSync bool AltSync bool
// Pubsub Scoring Parameters // Pubsub Scoring Parameters
PeerScoring pubsub.PeerScoreParams PeerScoring *pubsub.PeerScoreParams
TopicScoring pubsub.TopicScoreParams TopicScoring *pubsub.TopicScoreParams
// Whether to ban peers based on their [PeerScoring] score. Should be negative. // Whether to ban peers based on their [PeerScoring] score. Should be negative.
BanningEnabled bool BanningEnabled bool
...@@ -135,7 +135,7 @@ func (conf *Config) Disabled() bool { ...@@ -135,7 +135,7 @@ func (conf *Config) Disabled() bool {
} }
func (conf *Config) PeerScoringParams() *pubsub.PeerScoreParams { func (conf *Config) PeerScoringParams() *pubsub.PeerScoreParams {
return &conf.PeerScoring return conf.PeerScoring
} }
func (conf *Config) BanPeers() bool { func (conf *Config) BanPeers() bool {
...@@ -151,7 +151,7 @@ func (conf *Config) BanDuration() time.Duration { ...@@ -151,7 +151,7 @@ func (conf *Config) BanDuration() time.Duration {
} }
func (conf *Config) TopicScoringParams() *pubsub.TopicScoreParams { func (conf *Config) TopicScoringParams() *pubsub.TopicScoreParams {
return &conf.TopicScoring return conf.TopicScoring
} }
func (conf *Config) ReqRespSyncEnabled() bool { func (conf *Config) ReqRespSyncEnabled() bool {
......
...@@ -443,10 +443,7 @@ func JoinGossip(p2pCtx context.Context, self peer.ID, topicScoreParams *pubsub.T ...@@ -443,10 +443,7 @@ func JoinGossip(p2pCtx context.Context, self peer.ID, topicScoreParams *pubsub.T
} }
go LogTopicEvents(p2pCtx, log.New("topic", "blocks"), blocksTopicEvents) go LogTopicEvents(p2pCtx, log.New("topic", "blocks"), blocksTopicEvents)
// A [TimeInMeshQuantum] value of 0 means the topic score is disabled. if topicScoreParams != nil {
// If we passed a topicScoreParams with [TimeInMeshQuantum] set to 0,
// libp2p errors since the params will be rejected.
if topicScoreParams != nil && topicScoreParams.TimeInMeshQuantum != 0 {
if err = blocksTopic.SetScoreParams(topicScoreParams); err != nil { if err = blocksTopic.SetScoreParams(topicScoreParams); err != nil {
return nil, fmt.Errorf("failed to set topic score params: %w", err) return nil, fmt.Errorf("failed to set topic score params: %w", err)
} }
......
...@@ -146,7 +146,15 @@ func (conf *Config) Host(log log.Logger, reporter metrics.Reporter, metrics Host ...@@ -146,7 +146,15 @@ func (conf *Config) Host(log log.Logger, reporter metrics.Reporter, metrics Host
} }
peerScoreParams := conf.PeerScoringParams() peerScoreParams := conf.PeerScoringParams()
ps, err := store.NewExtendedPeerstore(context.Background(), log, clock.SystemClock, basePs, conf.Store, peerScoreParams.RetainScore) var scoreRetention time.Duration
if peerScoreParams != nil {
// Use the same retention period as gossip will if available
scoreRetention = peerScoreParams.RetainScore
} else {
// Disable score GC if peer scoring is disabled
scoreRetention = 0
}
ps, err := store.NewExtendedPeerstore(context.Background(), log, clock.SystemClock, basePs, conf.Store, scoreRetention)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open extended peerstore: %w", err) return nil, fmt.Errorf("failed to open extended peerstore: %w", err)
} }
......
...@@ -12,14 +12,13 @@ func ConfigurePeerScoring(gossipConf GossipSetupConfigurables, scorer Scorer, lo ...@@ -12,14 +12,13 @@ func ConfigurePeerScoring(gossipConf GossipSetupConfigurables, scorer Scorer, lo
peerScoreParams := gossipConf.PeerScoringParams() peerScoreParams := gossipConf.PeerScoringParams()
peerScoreThresholds := NewPeerScoreThresholds() peerScoreThresholds := NewPeerScoreThresholds()
opts := []pubsub.Option{} opts := []pubsub.Option{}
// Check the app specific score since libp2p doesn't export it's [validate] function :/ if peerScoreParams != nil {
if peerScoreParams != nil && peerScoreParams.AppSpecificScore != nil {
opts = []pubsub.Option{ opts = []pubsub.Option{
pubsub.WithPeerScore(peerScoreParams, &peerScoreThresholds), pubsub.WithPeerScore(peerScoreParams, &peerScoreThresholds),
pubsub.WithPeerScoreInspect(scorer.SnapshotHook(), peerScoreInspectFrequency), pubsub.WithPeerScoreInspect(scorer.SnapshotHook(), peerScoreInspectFrequency),
} }
} else { } else {
log.Warn("Proceeding with no peer scoring...\nMissing AppSpecificScore in peer scoring params") log.Info("Peer scoring disabled")
} }
return opts return opts
} }
...@@ -102,7 +102,7 @@ func newGossipSubs(testSuite *PeerScoresTestSuite, ctx context.Context, hosts [] ...@@ -102,7 +102,7 @@ func newGossipSubs(testSuite *PeerScoresTestSuite, ctx context.Context, hosts []
&rollup.Config{L2ChainID: big.NewInt(123)}, &rollup.Config{L2ChainID: big.NewInt(123)},
extPeerStore, testSuite.mockMetricer, logger) extPeerStore, testSuite.mockMetricer, logger)
opts = append(opts, ConfigurePeerScoring(&Config{ opts = append(opts, ConfigurePeerScoring(&Config{
PeerScoring: pubsub.PeerScoreParams{ PeerScoring: &pubsub.PeerScoreParams{
AppSpecificScore: func(p peer.ID) float64 { AppSpecificScore: func(p peer.ID) float64 {
if p == hosts[0].ID() { if p == hosts[0].ID() {
return -1000 return -1000
......
...@@ -142,7 +142,7 @@ func (cfg *Config) CheckL1ChainID(ctx context.Context, client L1Client) error { ...@@ -142,7 +142,7 @@ func (cfg *Config) CheckL1ChainID(ctx context.Context, client L1Client) error {
return err return err
} }
if cfg.L1ChainID.Cmp(id) != 0 { if cfg.L1ChainID.Cmp(id) != 0 {
return fmt.Errorf("incorrect L1 RPC chain id %d, expected %d", cfg.L1ChainID, id) return fmt.Errorf("incorrect L1 RPC chain id %d, expected %d", id, cfg.L1ChainID)
} }
return nil return nil
} }
...@@ -154,7 +154,7 @@ func (cfg *Config) CheckL1GenesisBlockHash(ctx context.Context, client L1Client) ...@@ -154,7 +154,7 @@ func (cfg *Config) CheckL1GenesisBlockHash(ctx context.Context, client L1Client)
return err return err
} }
if l1GenesisBlockRef.Hash != cfg.Genesis.L1.Hash { if l1GenesisBlockRef.Hash != cfg.Genesis.L1.Hash {
return fmt.Errorf("incorrect L1 genesis block hash %d, expected %d", cfg.Genesis.L1.Hash, l1GenesisBlockRef.Hash) return fmt.Errorf("incorrect L1 genesis block hash %s, expected %s", l1GenesisBlockRef.Hash, cfg.Genesis.L1.Hash)
} }
return nil return nil
} }
...@@ -171,7 +171,7 @@ func (cfg *Config) CheckL2ChainID(ctx context.Context, client L2Client) error { ...@@ -171,7 +171,7 @@ func (cfg *Config) CheckL2ChainID(ctx context.Context, client L2Client) error {
return err return err
} }
if cfg.L2ChainID.Cmp(id) != 0 { if cfg.L2ChainID.Cmp(id) != 0 {
return fmt.Errorf("incorrect L2 RPC chain id, expected from config %d, obtained from client %d", cfg.L2ChainID, id) return fmt.Errorf("incorrect L2 RPC chain id %d, expected %d", id, cfg.L2ChainID)
} }
return nil return nil
} }
...@@ -183,7 +183,7 @@ func (cfg *Config) CheckL2GenesisBlockHash(ctx context.Context, client L2Client) ...@@ -183,7 +183,7 @@ func (cfg *Config) CheckL2GenesisBlockHash(ctx context.Context, client L2Client)
return err return err
} }
if l2GenesisBlockRef.Hash != cfg.Genesis.L2.Hash { if l2GenesisBlockRef.Hash != cfg.Genesis.L2.Hash {
return fmt.Errorf("incorrect L2 genesis block hash %d, expected %d", cfg.Genesis.L2.Hash, l2GenesisBlockRef.Hash) return fmt.Errorf("incorrect L2 genesis block hash %s, expected %s", l2GenesisBlockRef.Hash, cfg.Genesis.L2.Hash)
} }
return nil return nil
} }
......
...@@ -27,11 +27,11 @@ CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597) ...@@ -27,11 +27,11 @@ CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597)
CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883) CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883)
DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582) DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582)
DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395) DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395)
DisputeGameFactory_Test:test_owner_succeeds() (gas: 7582) DisputeGameFactory_Test:test_owner_succeeds() (gas: 7627)
DisputeGameFactory_Test:test_setImplementation_notOwner_reverts() (gas: 11191) DisputeGameFactory_Test:test_setImplementation_notOwner_reverts() (gas: 11077)
DisputeGameFactory_Test:test_setImplementation_succeeds() (gas: 38765) DisputeGameFactory_Test:test_setImplementation_succeeds() (gas: 38320)
DisputeGameFactory_Test:test_transferOwnership_notOwner_reverts() (gas: 10979) DisputeGameFactory_Test:test_transferOwnership_notOwner_reverts() (gas: 10979)
DisputeGameFactory_Test:test_transferOwnership_succeeds() (gas: 13180) DisputeGameFactory_Test:test_transferOwnership_succeeds() (gas: 13225)
FeeVault_Test:test_constructor_succeeds() (gas: 18185) FeeVault_Test:test_constructor_succeeds() (gas: 18185)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2950342) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2950342)
......
...@@ -253,11 +253,12 @@ ...@@ -253,11 +253,12 @@
➡ contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory ➡ contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory
======================= =======================
| Name | Type | Slot | Offset | Bytes | Contract | | Name | Type | Slot | Offset | Bytes | Contract |
|--------------|-------------------------------------------------|------|--------|-------|-------------------------------------------------------------| |-----------------|--------------------------------------------|------|--------|-------|-------------------------------------------------------------|
| _owner | address | 0 | 0 | 20 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory | | _owner | address | 0 | 0 | 20 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory |
| gameImpls | mapping(enum GameType => contract IDisputeGame) | 1 | 0 | 32 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory | | gameImpls | mapping(GameType => contract IDisputeGame) | 1 | 0 | 32 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory |
| disputeGames | mapping(Hash => contract IDisputeGame) | 2 | 0 | 32 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory | | disputeGames | mapping(Hash => contract IDisputeGame) | 2 | 0 | 32 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory |
| disputeGameList | contract IDisputeGame[] | 3 | 0 | 32 | contracts/dispute/DisputeGameFactory.sol:DisputeGameFactory |
======================= =======================
➡ contracts/dispute/BondManager.sol:BondManager ➡ contracts/dispute/BondManager.sol:BondManager
......
...@@ -83,7 +83,7 @@ contract BondManager is IBondManager { ...@@ -83,7 +83,7 @@ contract BondManager is IBondManager {
IDisputeGame caller = IDisputeGame(msg.sender); IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games( IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION, GameTypes.ATTESTATION,
caller.rootClaim(), caller.rootClaim(),
caller.extraData() caller.extraData()
); );
...@@ -108,7 +108,7 @@ contract BondManager is IBondManager { ...@@ -108,7 +108,7 @@ contract BondManager is IBondManager {
IDisputeGame caller = IDisputeGame(msg.sender); IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games( IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION, GameTypes.ATTESTATION,
caller.rootClaim(), caller.rootClaim(),
caller.extraData() caller.extraData()
); );
......
...@@ -32,6 +32,13 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory { ...@@ -32,6 +32,13 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory {
*/ */
mapping(Hash => IDisputeGame) internal disputeGames; mapping(Hash => IDisputeGame) internal disputeGames;
/**
* @notice An append-only array of disputeGames that have been created.
* @dev This accessor is used by offchain game solvers to efficiently
* track dispute games
*/
IDisputeGame[] public disputeGameList;
/** /**
* @notice Constructs a new DisputeGameFactory contract. * @notice Constructs a new DisputeGameFactory contract.
* @param _owner The owner of the contract. * @param _owner The owner of the contract.
...@@ -40,6 +47,13 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory { ...@@ -40,6 +47,13 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory {
transferOwnership(_owner); transferOwnership(_owner);
} }
/**
* @inheritdoc IDisputeGameFactory
*/
function gameCount() external view returns (uint256 _gameCount) {
_gameCount = disputeGameList.length;
}
/** /**
* @inheritdoc IDisputeGameFactory * @inheritdoc IDisputeGameFactory
*/ */
...@@ -81,6 +95,7 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory { ...@@ -81,6 +95,7 @@ contract DisputeGameFactory is Ownable, IDisputeGameFactory {
// Store the dispute game in the mapping & emit the `DisputeGameCreated` event. // Store the dispute game in the mapping & emit the `DisputeGameCreated` event.
disputeGames[uuid] = proxy; disputeGames[uuid] = proxy;
disputeGameList.push(proxy);
emit DisputeGameCreated(address(proxy), gameType, rootClaim); emit DisputeGameCreated(address(proxy), gameType, rootClaim);
} }
......
...@@ -29,6 +29,12 @@ interface IDisputeGameFactory { ...@@ -29,6 +29,12 @@ interface IDisputeGameFactory {
*/ */
event ImplementationSet(address indexed impl, GameType indexed gameType); event ImplementationSet(address indexed impl, GameType indexed gameType);
/**
* @notice the total number of dispute games created by this factory.
* @return _gameCount The total number of dispute games created by this factory.
*/
function gameCount() external view returns (uint256 _gameCount);
/** /**
* @notice `games` queries an internal a mapping that maps the hash of * @notice `games` queries an internal a mapping that maps the hash of
* `gameType ++ rootClaim ++ extraData` to the deployed `DisputeGame` clone. * `gameType ++ rootClaim ++ extraData` to the deployed `DisputeGame` clone.
......
...@@ -57,6 +57,11 @@ type Clock is uint256; ...@@ -57,6 +57,11 @@ type Clock is uint256;
*/ */
type Position is uint256; type Position is uint256;
/**
* @notice A `GameType` represents the type of game being played.
*/
type GameType is uint8;
/** /**
* @notice The current status of the dispute game. * @notice The current status of the dispute game.
*/ */
...@@ -70,13 +75,22 @@ enum GameStatus { ...@@ -70,13 +75,22 @@ enum GameStatus {
} }
/** /**
* @notice The type of proof system being used. * @title GameTypes
* @notice A library that defines the IDs of games that can be played.
*/ */
enum GameType { library GameTypes {
// The game will use a `IDisputeGame` implementation that utilizes fault proofs. /**
FAULT, * @dev The game will use a `IDisputeGame` implementation that utilizes fault proofs.
// The game will use a `IDisputeGame` implementation that utilizes validity proofs. */
VALIDITY, GameType internal constant FAULT = GameType.wrap(0);
// The game will use a `IDisputeGame` implementation that utilizes attestation proofs.
ATTESTATION /**
* @dev The game will use a `IDisputeGame` implementation that utilizes validity proofs.
*/
GameType internal constant VALIDITY = GameType.wrap(1);
/**
* @dev The game will use a `IDisputeGame` implementation that utilizes attestation proofs.
*/
GameType internal constant ATTESTATION = GameType.wrap(2);
} }
...@@ -208,7 +208,7 @@ contract BondManager_Test is Test { ...@@ -208,7 +208,7 @@ contract BondManager_Test is Test {
{ {
rootClaim = Claim.wrap(bytes32("")); rootClaim = Claim.wrap(bytes32(""));
MockAttestationDisputeGame implementation = new MockAttestationDisputeGame(); MockAttestationDisputeGame implementation = new MockAttestationDisputeGame();
GameType gt = GameType.ATTESTATION; GameType gt = GameTypes.ATTESTATION;
factory.setImplementation(gt, IDisputeGame(address(implementation))); factory.setImplementation(gt, IDisputeGame(address(implementation)));
vm.expectEmit(false, true, true, false); vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim); emit DisputeGameCreated(address(0), gt, rootClaim);
...@@ -261,7 +261,7 @@ contract BondManager_Test is Test { ...@@ -261,7 +261,7 @@ contract BondManager_Test is Test {
{ {
rootClaim = Claim.wrap(bytes32("")); rootClaim = Claim.wrap(bytes32(""));
MockAttestationDisputeGame implementation = new MockAttestationDisputeGame(); MockAttestationDisputeGame implementation = new MockAttestationDisputeGame();
GameType gt = GameType.ATTESTATION; GameType gt = GameTypes.ATTESTATION;
factory.setImplementation(gt, IDisputeGame(address(implementation))); factory.setImplementation(gt, IDisputeGame(address(implementation)));
vm.expectEmit(false, true, true, false); vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim); emit DisputeGameCreated(address(0), gt, rootClaim);
...@@ -418,7 +418,7 @@ contract MockAttestationDisputeGame { ...@@ -418,7 +418,7 @@ contract MockAttestationDisputeGame {
} }
function gameType() external pure returns (GameType _gameType) { function gameType() external pure returns (GameType _gameType) {
return GameType.ATTESTATION; return GameTypes.ATTESTATION;
} }
function rootClaim() external view returns (Claim _rootClaim) { function rootClaim() external view returns (Claim _rootClaim) {
......
...@@ -35,11 +35,11 @@ contract DisputeGameFactory_Test is Test { ...@@ -35,11 +35,11 @@ contract DisputeGameFactory_Test is Test {
bytes calldata extraData bytes calldata extraData
) public { ) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2))); GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
// Set all three implementations to the same `FakeClone` contract. // Set all three implementations to the same `FakeClone` contract.
for (uint8 i; i < 3; i++) { for (uint8 i; i < 3; i++) {
factory.setImplementation(GameType(i), IDisputeGame(address(fakeClone))); factory.setImplementation(GameType.wrap(i), IDisputeGame(address(fakeClone)));
} }
vm.expectEmit(false, true, true, false); vm.expectEmit(false, true, true, false);
...@@ -48,6 +48,8 @@ contract DisputeGameFactory_Test is Test { ...@@ -48,6 +48,8 @@ contract DisputeGameFactory_Test is Test {
// Ensure that the dispute game was assigned to the `disputeGames` mapping. // Ensure that the dispute game was assigned to the `disputeGames` mapping.
assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy)); assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy));
assertEq(factory.gameCount(), 1);
assertEq(address(factory.disputeGameList(0)), address(proxy));
} }
/** /**
...@@ -60,7 +62,7 @@ contract DisputeGameFactory_Test is Test { ...@@ -60,7 +62,7 @@ contract DisputeGameFactory_Test is Test {
bytes calldata extraData bytes calldata extraData
) public { ) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2))); GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
vm.expectRevert(abi.encodeWithSelector(NoImplementation.selector, gt)); vm.expectRevert(abi.encodeWithSelector(NoImplementation.selector, gt));
factory.create(gt, rootClaim, extraData); factory.create(gt, rootClaim, extraData);
...@@ -75,11 +77,11 @@ contract DisputeGameFactory_Test is Test { ...@@ -75,11 +77,11 @@ contract DisputeGameFactory_Test is Test {
bytes calldata extraData bytes calldata extraData
) public { ) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2))); GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
// Set all three implementations to the same `FakeClone` contract. // Set all three implementations to the same `FakeClone` contract.
for (uint8 i; i < 3; i++) { for (uint8 i; i < 3; i++) {
factory.setImplementation(GameType(i), IDisputeGame(address(fakeClone))); factory.setImplementation(GameType.wrap(i), IDisputeGame(address(fakeClone)));
} }
// Create our first dispute game - this should succeed. // Create our first dispute game - this should succeed.
...@@ -104,17 +106,17 @@ contract DisputeGameFactory_Test is Test { ...@@ -104,17 +106,17 @@ contract DisputeGameFactory_Test is Test {
* @dev Tests that the `setImplementation` function properly sets the implementation for a given `GameType`. * @dev Tests that the `setImplementation` function properly sets the implementation for a given `GameType`.
*/ */
function test_setImplementation_succeeds() public { function test_setImplementation_succeeds() public {
// There should be no implementation for the `GameType.FAULT` enum value, it has not been set. // There should be no implementation for the `GameTypes.FAULT` enum value, it has not been set.
assertEq(address(factory.gameImpls(GameType.FAULT)), address(0)); assertEq(address(factory.gameImpls(GameTypes.FAULT)), address(0));
vm.expectEmit(true, true, true, true, address(factory)); vm.expectEmit(true, true, true, true, address(factory));
emit ImplementationSet(address(1), GameType.FAULT); emit ImplementationSet(address(1), GameTypes.FAULT);
// Set the implementation for the `GameType.FAULT` enum value. // Set the implementation for the `GameTypes.FAULT` enum value.
factory.setImplementation(GameType.FAULT, IDisputeGame(address(1))); factory.setImplementation(GameTypes.FAULT, IDisputeGame(address(1)));
// Ensure that the implementation for the `GameType.FAULT` enum value is set. // Ensure that the implementation for the `GameTypes.FAULT` enum value is set.
assertEq(address(factory.gameImpls(GameType.FAULT)), address(1)); assertEq(address(factory.gameImpls(GameTypes.FAULT)), address(1));
} }
/** /**
...@@ -124,7 +126,7 @@ contract DisputeGameFactory_Test is Test { ...@@ -124,7 +126,7 @@ contract DisputeGameFactory_Test is Test {
// Ensure that the `setImplementation` function reverts when called by a non-owner. // Ensure that the `setImplementation` function reverts when called by a non-owner.
vm.prank(address(0)); vm.prank(address(0));
vm.expectRevert("Ownable: caller is not the owner"); vm.expectRevert("Ownable: caller is not the owner");
factory.setImplementation(GameType.FAULT, IDisputeGame(address(1))); factory.setImplementation(GameTypes.FAULT, IDisputeGame(address(1)));
} }
/** /**
...@@ -137,7 +139,7 @@ contract DisputeGameFactory_Test is Test { ...@@ -137,7 +139,7 @@ contract DisputeGameFactory_Test is Test {
bytes calldata extraData bytes calldata extraData
) public { ) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
GameType gt = GameType(uint8(bound(gameType, 0, 2))); GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
assertEq( assertEq(
Hash.unwrap(factory.getGameUUID(gt, rootClaim, extraData)), Hash.unwrap(factory.getGameUUID(gt, rootClaim, extraData)),
......
...@@ -89,6 +89,52 @@ Cache use Redis and can be enabled for the following immutable methods: ...@@ -89,6 +89,52 @@ Cache use Redis and can be enabled for the following immutable methods:
* `eth_getUncleByBlockHashAndIndex` * `eth_getUncleByBlockHashAndIndex`
* `debug_getRawReceipts` (block hash only) * `debug_getRawReceipts` (block hash only)
## Meta method `consensus_getReceipts`
To support backends with different specifications in the same backend group,
proxyd exposes a convenient method to fetch receipts abstracting away
what specific backend will serve the request.
Each backend specifies their preferred method to fetch receipts with `consensus_receipts_target` config,
which will be translated from `consensus_getReceipts`.
This method takes a `blockNumberOrHash` (i.e. `tag|qty|hash`)
and returns the receipts for all transactions in the block.
Request example
```json
{
"jsonrpc":"2.0",
"id": 1,
"params": ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"]
}
```
It currently supports translation to the following targets:
* `debug_getRawReceipts(blockOrHash)` (default)
* `alchemy_getTransactionReceipts(blockOrHash)`
* `parity_getBlockReceipts(blockOrHash)`
* `eth_getBlockReceipts(blockOrHash)`
The selected target is returned in the response, in a wrapped result.
Response example
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"method": "debug_getRawReceipts",
"result": {
// the actual raw result from backend
}
}
}
```
See [op-node receipt fetcher](https://github.com/ethereum-optimism/optimism/blob/186e46a47647a51a658e699e9ff047d39444c2de/op-node/sources/receipts.go#L186-L253).
## Metrics ## Metrics
See `metrics.go` for a list of all available metrics. See `metrics.go` for a list of all available metrics.
......
...@@ -18,6 +18,8 @@ import ( ...@@ -18,6 +18,8 @@ import (
"time" "time"
sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
...@@ -97,6 +99,9 @@ var ( ...@@ -97,6 +99,9 @@ var (
} }
ErrBackendUnexpectedJSONRPC = errors.New("backend returned an unexpected JSON-RPC response") ErrBackendUnexpectedJSONRPC = errors.New("backend returned an unexpected JSON-RPC response")
ErrConsensusGetReceiptsCantBeBatched = errors.New("consensus_getReceipts cannot be batched")
ErrConsensusGetReceiptsInvalidTarget = errors.New("unsupported consensus_receipts_target")
) )
func ErrInvalidRequest(msg string) *RPCErr { func ErrInvalidRequest(msg string) *RPCErr {
...@@ -118,6 +123,7 @@ func ErrInvalidParams(msg string) *RPCErr { ...@@ -118,6 +123,7 @@ func ErrInvalidParams(msg string) *RPCErr {
type Backend struct { type Backend struct {
Name string Name string
rpcURL string rpcURL string
receiptsTarget string
wsURL string wsURL string
authUsername string authUsername string
authPassword string authPassword string
...@@ -208,7 +214,7 @@ func WithProxydIP(ip string) BackendOpt { ...@@ -208,7 +214,7 @@ func WithProxydIP(ip string) BackendOpt {
} }
} }
func WithSkipPeerCountCheck(skipPeerCountCheck bool) BackendOpt { func WithConsensusSkipPeerCountCheck(skipPeerCountCheck bool) BackendOpt {
return func(b *Backend) { return func(b *Backend) {
b.skipPeerCountCheck = skipPeerCountCheck b.skipPeerCountCheck = skipPeerCountCheck
} }
...@@ -232,12 +238,36 @@ func WithMaxErrorRateThreshold(maxErrorRateThreshold float64) BackendOpt { ...@@ -232,12 +238,36 @@ func WithMaxErrorRateThreshold(maxErrorRateThreshold float64) BackendOpt {
} }
} }
func WithConsensusReceiptTarget(receiptsTarget string) BackendOpt {
return func(b *Backend) {
b.receiptsTarget = receiptsTarget
}
}
type indexedReqRes struct { type indexedReqRes struct {
index int index int
req *RPCReq req *RPCReq
res *RPCRes res *RPCRes
} }
const ConsensusGetReceiptsMethod = "consensus_getReceipts"
const ReceiptsTargetDebugGetRawReceipts = "debug_getRawReceipts"
const ReceiptsTargetAlchemyGetTransactionReceipts = "alchemy_getTransactionReceipts"
const ReceiptsTargetParityGetTransactionReceipts = "parity_getBlockReceipts"
const ReceiptsTargetEthGetTransactionReceipts = "eth_getBlockReceipts"
type ConsensusGetReceiptsResult struct {
Method string `json:"method"`
Result interface{} `json:"result"`
}
// BlockHashOrNumberParameter is a non-conventional wrapper used by alchemy_getTransactionReceipts
type BlockHashOrNumberParameter struct {
BlockHash *common.Hash `json:"blockHash"`
BlockNumber *rpc.BlockNumber `json:"blockNumber"`
}
func NewBackend( func NewBackend(
name string, name string,
rpcURL string, rpcURL string,
...@@ -266,9 +296,7 @@ func NewBackend( ...@@ -266,9 +296,7 @@ func NewBackend(
networkErrorsSlidingWindow: sw.NewSlidingWindow(), networkErrorsSlidingWindow: sw.NewSlidingWindow(),
} }
for _, opt := range opts { backend.Override(opts...)
opt(backend)
}
if !backend.stripTrailingXFF && backend.proxydIP == "" { if !backend.stripTrailingXFF && backend.proxydIP == "" {
log.Warn("proxied requests' XFF header will not contain the proxyd ip address") log.Warn("proxied requests' XFF header will not contain the proxyd ip address")
...@@ -277,6 +305,12 @@ func NewBackend( ...@@ -277,6 +305,12 @@ func NewBackend(
return backend return backend
} }
func (b *Backend) Override(opts ...BackendOpt) {
for _, opt := range opts {
opt(b)
}
}
func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([]*RPCRes, error) { func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([]*RPCRes, error) {
var lastError error var lastError error
// <= to account for the first attempt not technically being // <= to account for the first attempt not technically being
...@@ -298,6 +332,20 @@ func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([] ...@@ -298,6 +332,20 @@ func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([]
res, err := b.doForward(ctx, reqs, isBatch) res, err := b.doForward(ctx, reqs, isBatch)
switch err { switch err {
case nil: // do nothing case nil: // do nothing
case ErrConsensusGetReceiptsCantBeBatched:
log.Warn(
"Received unsupported batch request for consensus_getReceipts",
"name", b.Name,
"req_id", GetReqID(ctx),
"err", err,
)
case ErrConsensusGetReceiptsInvalidTarget:
log.Error(
"Unsupported consensus_receipts_target for consensus_getReceipts",
"name", b.Name,
"req_id", GetReqID(ctx),
"err", err,
)
// ErrBackendUnexpectedJSONRPC occurs because infura responds with a single JSON-RPC object // ErrBackendUnexpectedJSONRPC occurs because infura responds with a single JSON-RPC object
// to a batch request whenever any Request Object in the batch would induce a partial error. // to a batch request whenever any Request Object in the batch would induce a partial error.
// We don't label the backend offline in this case. But the error is still returned to // We don't label the backend offline in this case. But the error is still returned to
...@@ -375,11 +423,63 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool ...@@ -375,11 +423,63 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool
// we are concerned about network error rates, so we record 1 request independently of how many are in the batch // we are concerned about network error rates, so we record 1 request independently of how many are in the batch
b.networkRequestsSlidingWindow.Incr() b.networkRequestsSlidingWindow.Incr()
translatedReqs := make(map[string]*RPCReq, len(rpcReqs))
// translate consensus_getReceipts to receipts target
// right now we only support non-batched
if isBatch {
for _, rpcReq := range rpcReqs {
if rpcReq.Method == ConsensusGetReceiptsMethod {
return nil, ErrConsensusGetReceiptsCantBeBatched
}
}
} else {
for _, rpcReq := range rpcReqs {
if rpcReq.Method == ConsensusGetReceiptsMethod {
translatedReqs[string(rpcReq.ID)] = rpcReq
rpcReq.Method = b.receiptsTarget
var reqParams []rpc.BlockNumberOrHash
err := json.Unmarshal(rpcReq.Params, &reqParams)
if err != nil {
return nil, ErrInvalidRequest("invalid request")
}
var translatedParams []byte
switch rpcReq.Method {
case ReceiptsTargetDebugGetRawReceipts,
ReceiptsTargetEthGetTransactionReceipts,
ReceiptsTargetParityGetTransactionReceipts:
// conventional methods use an array of strings having either block number or block hash
// i.e. ["0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"]
params := make([]string, 1)
if reqParams[0].BlockNumber != nil {
params[0] = reqParams[0].BlockNumber.String()
} else {
params[0] = reqParams[0].BlockHash.Hex()
}
translatedParams = mustMarshalJSON(params)
case ReceiptsTargetAlchemyGetTransactionReceipts:
// alchemy uses an array of object with either block number or block hash
// i.e. [{ blockHash: "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b" }]
params := make([]BlockHashOrNumberParameter, 1)
if reqParams[0].BlockNumber != nil {
params[0].BlockNumber = reqParams[0].BlockNumber
} else {
params[0].BlockHash = reqParams[0].BlockHash
}
translatedParams = mustMarshalJSON(params)
default:
return nil, ErrConsensusGetReceiptsInvalidTarget
}
rpcReq.Params = translatedParams
}
}
}
isSingleElementBatch := len(rpcReqs) == 1 isSingleElementBatch := len(rpcReqs) == 1
// Single element batches are unwrapped before being sent // Single element batches are unwrapped before being sent
// since Alchemy handles single requests better than batches. // since Alchemy handles single requests better than batches.
var body []byte var body []byte
if isSingleElementBatch { if isSingleElementBatch {
body = mustMarshalJSON(rpcReqs[0]) body = mustMarshalJSON(rpcReqs[0])
...@@ -443,17 +543,17 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool ...@@ -443,17 +543,17 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool
return nil, wrapErr(err, "error reading response body") return nil, wrapErr(err, "error reading response body")
} }
var res []*RPCRes var rpcRes []*RPCRes
if isSingleElementBatch { if isSingleElementBatch {
var singleRes RPCRes var singleRes RPCRes
if err := json.Unmarshal(resB, &singleRes); err != nil { if err := json.Unmarshal(resB, &singleRes); err != nil {
return nil, ErrBackendBadResponse return nil, ErrBackendBadResponse
} }
res = []*RPCRes{ rpcRes = []*RPCRes{
&singleRes, &singleRes,
} }
} else { } else {
if err := json.Unmarshal(resB, &res); err != nil { if err := json.Unmarshal(resB, &rpcRes); err != nil {
// Infura may return a single JSON-RPC response if, for example, the batch contains a request for an unsupported method // Infura may return a single JSON-RPC response if, for example, the batch contains a request for an unsupported method
if responseIsNotBatched(resB) { if responseIsNotBatched(resB) {
b.networkErrorsSlidingWindow.Incr() b.networkErrorsSlidingWindow.Incr()
...@@ -466,7 +566,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool ...@@ -466,7 +566,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool
} }
} }
if len(rpcReqs) != len(res) { if len(rpcReqs) != len(rpcRes) {
b.networkErrorsSlidingWindow.Incr() b.networkErrorsSlidingWindow.Incr()
RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate()) RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate())
return nil, ErrBackendUnexpectedJSONRPC return nil, ErrBackendUnexpectedJSONRPC
...@@ -475,7 +575,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool ...@@ -475,7 +575,7 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool
// capture the HTTP status code in the response. this will only // capture the HTTP status code in the response. this will only
// ever be 400 given the status check on line 318 above. // ever be 400 given the status check on line 318 above.
if httpRes.StatusCode != 200 { if httpRes.StatusCode != 200 {
for _, res := range res { for _, res := range rpcRes {
res.Error.HTTPErrorCode = httpRes.StatusCode res.Error.HTTPErrorCode = httpRes.StatusCode
} }
} }
...@@ -484,8 +584,20 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool ...@@ -484,8 +584,20 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool
RecordBackendNetworkLatencyAverageSlidingWindow(b, time.Duration(b.latencySlidingWindow.Avg())) RecordBackendNetworkLatencyAverageSlidingWindow(b, time.Duration(b.latencySlidingWindow.Avg()))
RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate()) RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate())
sortBatchRPCResponse(rpcReqs, res) // enrich the response with the actual request method
return res, nil for _, res := range rpcRes {
translatedReq, exist := translatedReqs[string(res.ID)]
if exist {
res.Result = ConsensusGetReceiptsResult{
Method: translatedReq.Method,
Result: res.Result,
}
}
}
sortBatchRPCResponse(rpcReqs, rpcRes)
return rpcRes, nil
} }
// IsHealthy checks if the backend is able to serve traffic, based on dynamic parameters // IsHealthy checks if the backend is able to serve traffic, based on dynamic parameters
...@@ -604,7 +716,9 @@ func (bg *BackendGroup) Forward(ctx context.Context, rpcReqs []*RPCReq, isBatch ...@@ -604,7 +716,9 @@ func (bg *BackendGroup) Forward(ctx context.Context, rpcReqs []*RPCReq, isBatch
if len(rpcReqs) > 0 { if len(rpcReqs) > 0 {
res, err = back.Forward(ctx, rpcReqs, isBatch) res, err = back.Forward(ctx, rpcReqs, isBatch)
if errors.Is(err, ErrMethodNotWhitelisted) { if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) ||
errors.Is(err, ErrMethodNotWhitelisted) {
return nil, err return nil, err
} }
if errors.Is(err, ErrBackendOffline) { if errors.Is(err, ErrBackendOffline) {
......
...@@ -79,18 +79,20 @@ type BackendOptions struct { ...@@ -79,18 +79,20 @@ type BackendOptions struct {
} }
type BackendConfig struct { type BackendConfig struct {
Username string `toml:"username"` Username string `toml:"username"`
Password string `toml:"password"` Password string `toml:"password"`
RPCURL string `toml:"rpc_url"` RPCURL string `toml:"rpc_url"`
WSURL string `toml:"ws_url"` WSURL string `toml:"ws_url"`
WSPort int `toml:"ws_port"` WSPort int `toml:"ws_port"`
MaxRPS int `toml:"max_rps"` MaxRPS int `toml:"max_rps"`
MaxWSConns int `toml:"max_ws_conns"` MaxWSConns int `toml:"max_ws_conns"`
CAFile string `toml:"ca_file"` CAFile string `toml:"ca_file"`
ClientCertFile string `toml:"client_cert_file"` ClientCertFile string `toml:"client_cert_file"`
ClientKeyFile string `toml:"client_key_file"` ClientKeyFile string `toml:"client_key_file"`
StripTrailingXFF bool `toml:"strip_trailing_xff"` StripTrailingXFF bool `toml:"strip_trailing_xff"`
SkipPeerCountCheck bool `toml:"consensus_skip_peer_count"`
ConsensusSkipPeerCountCheck bool `toml:"consensus_skip_peer_count"`
ConsensusReceiptsTarget string `toml:"consensus_receipts_target"`
} }
type BackendsConfig map[string]*BackendConfig type BackendsConfig map[string]*BackendConfig
......
...@@ -74,7 +74,9 @@ client_cert_file = "" ...@@ -74,7 +74,9 @@ client_cert_file = ""
client_key_file = "" client_key_file = ""
# Allows backends to skip peer count checking, default false # Allows backends to skip peer count checking, default false
# consensus_skip_peer_count = true # consensus_skip_peer_count = true
# Specified the target method to get receipts, default "debug_getRawReceipts"
# See https://github.com/ethereum-optimism/optimism/blob/186e46a47647a51a658e699e9ff047d39444c2de/op-node/sources/receipts.go#L186-L253
consensus_receipts_target = "eth_getBlockReceipts"
[backends.alchemy] [backends.alchemy]
rpc_url = "" rpc_url = ""
...@@ -83,6 +85,7 @@ username = "" ...@@ -83,6 +85,7 @@ username = ""
password = "" password = ""
max_rps = 3 max_rps = 3
max_ws_conns = 1 max_ws_conns = 1
consensus_receipts_target = "alchemy_getTransactionReceipts"
[backend_groups] [backend_groups]
[backend_groups.main] [backend_groups.main]
......
...@@ -9,6 +9,7 @@ require ( ...@@ -9,6 +9,7 @@ require (
github.com/ethereum/go-ethereum v1.12.0 github.com/ethereum/go-ethereum v1.12.0
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
......
...@@ -157,6 +157,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= ...@@ -157,6 +157,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
......
...@@ -784,6 +784,211 @@ func TestConsensus(t *testing.T) { ...@@ -784,6 +784,211 @@ func TestConsensus(t *testing.T) {
// dont rewrite for 0xe1 // dont rewrite for 0xe1
require.Equal(t, "0xe1", jsonMap[2]["result"].(map[string]interface{})["number"]) require.Equal(t, "0xe1", jsonMap[2]["result"].(map[string]interface{})["number"])
}) })
t.Run("translate consensus_getReceipts to debug_getRawReceipts", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var jsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", jsonMap["method"])
require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", jsonMap["params"].([]interface{})[0])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to debug_getRawReceipts with latest block tag", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"latest"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var jsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", jsonMap["method"])
require.Equal(t, "0x101", jsonMap["params"].([]interface{})[0])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to debug_getRawReceipts with block number", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"0x55"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var jsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &jsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", jsonMap["method"])
require.Equal(t, "0x55", jsonMap["params"].([]interface{})[0])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "debug_getRawReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with block hash", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts"))
defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts"))
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var reqJsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"])
require.Equal(t, "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockHash"])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with block number", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts"))
defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts"))
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"0x55"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var reqJsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"])
require.Equal(t, "0x55", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockNumber"])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to alchemy_getTransactionReceipts with latest block tag", func(t *testing.T) {
reset()
useOnlyNode1()
update()
// reset request counts
nodes["node1"].mockBackend.Reset()
nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("alchemy_getTransactionReceipts"))
defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts"))
resRaw, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"latest"})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
var reqJsonMap map[string]interface{}
err = json.Unmarshal(nodes["node1"].mockBackend.Requests()[0].Body, &reqJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", reqJsonMap["method"])
require.Equal(t, "0x101", reqJsonMap["params"].([]interface{})[0].(map[string]interface{})["blockNumber"])
var resJsonMap map[string]interface{}
err = json.Unmarshal(resRaw, &resJsonMap)
require.NoError(t, err)
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["method"].(string))
require.Equal(t, "alchemy_getTransactionReceipts", resJsonMap["result"].(map[string]interface{})["result"].(map[string]interface{})["_"])
})
t.Run("translate consensus_getReceipts to unsupported consensus_receipts_target", func(t *testing.T) {
reset()
useOnlyNode1()
nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("unsupported_consensus_receipts_target"))
defer nodes["node1"].backend.Override(proxyd.WithConsensusReceiptTarget("debug_getRawReceipts"))
_, statusCode, err := client.SendRPC("consensus_getReceipts",
[]interface{}{"latest"})
require.NoError(t, err)
require.Equal(t, 400, statusCode)
})
t.Run("consensus_getReceipts should not be used in a batch", func(t *testing.T) {
reset()
useOnlyNode1()
_, statusCode, err := client.SendBatchRPC(
NewRPCReq("1", "eth_getBlockByNumber", []interface{}{"latest"}),
NewRPCReq("2", "consensus_getReceipts", []interface{}{"0x55"}),
NewRPCReq("3", "eth_getBlockByNumber", []interface{}{"0xe1"}))
require.NoError(t, err)
require.Equal(t, 400, statusCode)
})
} }
func buildResponse(result interface{}) string { func buildResponse(result interface{}) string {
......
...@@ -27,3 +27,4 @@ eth_call = "node" ...@@ -27,3 +27,4 @@ eth_call = "node"
eth_chainId = "node" eth_chainId = "node"
eth_blockNumber = "node" eth_blockNumber = "node"
eth_getBlockByNumber = "node" eth_getBlockByNumber = "node"
consensus_getReceipts = "node"
...@@ -184,3 +184,30 @@ ...@@ -184,3 +184,30 @@
"number": "0xd1" "number": "0xd1"
} }
} }
- method: debug_getRawReceipts
response: >
{
"jsonrpc": "2.0",
"id": 67,
"result": {
"_": "debug_getRawReceipts"
}
}
- method: eth_getTransactionReceipt
response: >
{
"jsonrpc": "2.0",
"id": 67,
"result": {
"_": "eth_getTransactionReceipt"
}
}
- method: alchemy_getTransactionReceipts
response: >
{
"jsonrpc": "2.0",
"id": 67,
"result": {
"_": "alchemy_getTransactionReceipts"
}
}
...@@ -141,7 +141,17 @@ func Start(config *Config) (*Server, func(), error) { ...@@ -141,7 +141,17 @@ func Start(config *Config) (*Server, func(), error) {
opts = append(opts, WithStrippedTrailingXFF()) opts = append(opts, WithStrippedTrailingXFF())
} }
opts = append(opts, WithProxydIP(os.Getenv("PROXYD_IP"))) opts = append(opts, WithProxydIP(os.Getenv("PROXYD_IP")))
opts = append(opts, WithSkipPeerCountCheck(cfg.SkipPeerCountCheck)) opts = append(opts, WithConsensusSkipPeerCountCheck(cfg.ConsensusSkipPeerCountCheck))
receiptsTarget, err := ReadFromEnvOrConfig(cfg.ConsensusReceiptsTarget)
if err != nil {
return nil, nil, err
}
receiptsTarget, err = validateReceiptsTarget(receiptsTarget)
if err != nil {
return nil, nil, err
}
opts = append(opts, WithConsensusReceiptTarget(receiptsTarget))
back := NewBackend(name, rpcURL, wsURL, rpcRequestSemaphore, opts...) back := NewBackend(name, rpcURL, wsURL, rpcRequestSemaphore, opts...)
backendNames = append(backendNames, name) backendNames = append(backendNames, name)
...@@ -316,6 +326,21 @@ func Start(config *Config) (*Server, func(), error) { ...@@ -316,6 +326,21 @@ func Start(config *Config) (*Server, func(), error) {
return srv, shutdownFunc, nil return srv, shutdownFunc, nil
} }
func validateReceiptsTarget(val string) (string, error) {
if val == "" {
val = ReceiptsTargetDebugGetRawReceipts
}
switch val {
case ReceiptsTargetDebugGetRawReceipts,
ReceiptsTargetAlchemyGetTransactionReceipts,
ReceiptsTargetEthGetTransactionReceipts,
ReceiptsTargetParityGetTransactionReceipts:
return val, nil
default:
return "", fmt.Errorf("invalid receipts target: %s", val)
}
}
func secondsToDuration(seconds int) time.Duration { func secondsToDuration(seconds int) time.Duration {
return time.Duration(seconds) * time.Second return time.Duration(seconds) * time.Second
} }
......
...@@ -63,7 +63,7 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul ...@@ -63,7 +63,7 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul
case "eth_getLogs", case "eth_getLogs",
"eth_newFilter": "eth_newFilter":
return rewriteRange(rctx, req, res, 0) return rewriteRange(rctx, req, res, 0)
case "debug_getRawReceipts": case "debug_getRawReceipts", "consensus_getReceipts":
return rewriteParam(rctx, req, res, 0, true) return rewriteParam(rctx, req, res, 0, true)
case "eth_getBalance", case "eth_getBalance",
"eth_getCode", "eth_getCode",
......
...@@ -347,6 +347,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { ...@@ -347,6 +347,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
writeRPCError(ctx, w, nil, ErrGatewayTimeout) writeRPCError(ctx, w, nil, ErrGatewayTimeout)
return return
} }
if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) {
writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error()))
return
}
if err != nil { if err != nil {
writeRPCError(ctx, w, nil, ErrInternal) writeRPCError(ctx, w, nil, ErrInternal)
return return
...@@ -360,6 +365,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { ...@@ -360,6 +365,11 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
rawBody := json.RawMessage(body) rawBody := json.RawMessage(body)
backendRes, cached, err := s.handleBatchRPC(ctx, []json.RawMessage{rawBody}, isLimited, false) backendRes, cached, err := s.handleBatchRPC(ctx, []json.RawMessage{rawBody}, isLimited, false)
if err != nil { if err != nil {
if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) {
writeRPCError(ctx, w, nil, ErrInvalidRequest(err.Error()))
return
}
writeRPCError(ctx, w, nil, ErrInternal) writeRPCError(ctx, w, nil, ErrInternal)
return return
} }
...@@ -485,6 +495,10 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL ...@@ -485,6 +495,10 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
elems := cacheMisses[start:end] elems := cacheMisses[start:end]
res, err := s.BackendGroups[group.backendGroup].Forward(ctx, createBatchRequest(elems), isBatch) res, err := s.BackendGroups[group.backendGroup].Forward(ctx, createBatchRequest(elems), isBatch)
if err != nil { if err != nil {
if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) {
return nil, false, err
}
log.Error( log.Error(
"error forwarding RPC batch", "error forwarding RPC batch",
"batch_size", len(elems), "batch_size", len(elems),
......
...@@ -88,7 +88,15 @@ func (mh *MockedHandler) Handler(w http.ResponseWriter, req *http.Request) { ...@@ -88,7 +88,15 @@ func (mh *MockedHandler) Handler(w http.ResponseWriter, req *http.Request) {
} }
} }
if selectedResponse != "" { if selectedResponse != "" {
responses = append(responses, selectedResponse) var rpcRes proxyd.RPCRes
err = json.Unmarshal([]byte(selectedResponse), &rpcRes)
if err != nil {
panic(err)
}
idJson, _ := json.Marshal(r["id"])
rpcRes.ID = idJson
res, _ := json.Marshal(rpcRes)
responses = append(responses, string(res))
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment