Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isthmus: Updates for L2 withdrawals root in header #12848

Closed
wants to merge 18 commits into from
Closed
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ require (
rsc.io/tmplfunc v0.0.3 // indirect
)

replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101411.5-rc.1
replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101411.5-rc.1.0.20250102190045-58aed221675c

//replace github.com/ethereum/go-ethereum => ../go-ethereum

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/u
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs=
github.com/ethereum-optimism/op-geth v1.101411.5-rc.1 h1:8fhtAycm/+xugWev5jInUxgF0Wdc29PxSODZXca6Qi8=
github.com/ethereum-optimism/op-geth v1.101411.5-rc.1/go.mod h1:n6VeI9cKFxmXCauD7Ji9lgTAg+2TYGLZu5AXgVJB4tk=
github.com/ethereum-optimism/op-geth v1.101411.5-rc.1.0.20250102190045-58aed221675c h1:IBK8ZG8+zmIGGw4UtyhQMhDZIO0jqUjC7RZ0iFcS1Ow=
github.com/ethereum-optimism/op-geth v1.101411.5-rc.1.0.20250102190045-58aed221675c/go.mod h1:n6VeI9cKFxmXCauD7Ji9lgTAg+2TYGLZu5AXgVJB4tk=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241213092551-33a63fce8214 h1:94dIMFDCafAQ3FCC1pryuhgfZc1jPoDwK4xSMOPshN8=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241213092551-33a63fce8214/go.mod h1:9feO8jcL5OZ1tvRjEfNAHz4Aggvd6373l+ZxmZZAyZs=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
Expand Down
3 changes: 2 additions & 1 deletion op-chain-ops/cmd/check-derivation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ func newClientsFromContext(cliCtx *cli.Context) (*ethclient.Client, *sources.Eth
MethodResetDuration: time.Minute,
}
cl := ethclient.NewClient(clients.L2RpcClient)
ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(clients.L2RpcClient), log.Root(), nil, &ethClCfg)
l2RpcChecker := sources.NewL2RPCChecker()
ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(clients.L2RpcClient), log.Root(), nil, &ethClCfg, l2RpcChecker)
if err != nil {
return nil, nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion op-conductor/conductor/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ func (c *OpConductor) initSequencerControl(ctx context.Context) error {
return errors.Wrap(err, "failed to create geth rpc client")
}
execCfg := sources.L2ClientDefaultConfig(&c.cfg.RollupCfg, true)
l2RpcChecker := sources.NewL2RPCChecker()
// TODO: Add metrics tracer here. tracked by https://github.com/ethereum-optimism/protocol-quest/issues/45
exec, err := sources.NewEthClient(ec, c.log, nil, &execCfg.EthClientConfig)
exec, err := sources.NewEthClient(ec, c.log, nil, &execCfg.EthClientConfig, l2RpcChecker)
if err != nil {
return errors.Wrap(err, "failed to create geth client")
}
Expand Down
4 changes: 2 additions & 2 deletions op-e2e/actions/helpers/l2_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestL2EngineAPI(gt *testing.T) {
}
})

payloadA, err := eth.BlockAsPayloadEnv(chainA[0], sd.RollupCfg.CanyonTime)
payloadA, err := eth.BlockAsPayloadEnv(chainA[0], sd.L2Cfg.Config)
require.NoError(t, err)

// apply the payload
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestL2EngineAPI(gt *testing.T) {
}
})

payloadB, err := eth.BlockAsPayloadEnv(chainB[0], sd.RollupCfg.CanyonTime)
payloadB, err := eth.BlockAsPayloadEnv(chainB[0], sd.L2Cfg.Config)
require.NoError(t, err)

// apply the payload
Expand Down
14 changes: 14 additions & 0 deletions op-e2e/actions/helpers/l2_sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ func (s *L2Sequencer) ActBuildL2ToTime(t Testing, target uint64) {
}
}

func (s *L2Sequencer) ActBuildL2ToCanyon(t Testing) {
require.NotNil(t, s.RollupCfg.CanyonTime, "cannot activate CanyonTime when it is not scheduled")
for s.L2Unsafe().Time < *s.RollupCfg.CanyonTime {
s.ActL2EmptyBlock(t)
}
}

func (s *L2Sequencer) ActBuildL2ToEcotone(t Testing) {
require.NotNil(t, s.RollupCfg.EcotoneTime, "cannot activate Ecotone when it is not scheduled")
for s.L2Unsafe().Time < *s.RollupCfg.EcotoneTime {
Expand Down Expand Up @@ -233,3 +240,10 @@ func (s *L2Sequencer) ActBuildL2ToHolocene(t Testing) {
s.ActL2EmptyBlock(t)
}
}

func (s *L2Sequencer) ActBuildL2ToIsthmus(t Testing) {
require.NotNil(t, s.RollupCfg.IsthmusTime, "cannot activate IsthmusTime when it is not scheduled")
for s.L2Unsafe().Time < *s.RollupCfg.IsthmusTime {
s.ActL2EmptyBlock(t)
}
}
5 changes: 5 additions & 0 deletions op-e2e/actions/helpers/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type hardforkScheduledTest struct {
fjordTime *hexutil.Uint64
graniteTime *hexutil.Uint64
holoceneTime *hexutil.Uint64
isthmusTime *hexutil.Uint64
runToFork string
allocType config.AllocType
}
Expand All @@ -41,6 +42,8 @@ func (tc *hardforkScheduledTest) GetFork(fork string) *uint64 {

func (tc *hardforkScheduledTest) fork(fork string) **hexutil.Uint64 {
switch fork {
case "isthmus":
return &tc.isthmusTime
case "holocene":
return &tc.holoceneTime
case "granite":
Expand Down Expand Up @@ -88,6 +91,7 @@ func testCrossLayerUser(t *testing.T, allocType config.AllocType) {
"fjord",
"granite",
"holocene",
"isthmus",
}
for i, fork := range forks {
i := i
Expand Down Expand Up @@ -146,6 +150,7 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) {
dp.DeployConfig.L2GenesisFjordTimeOffset = test.fjordTime
dp.DeployConfig.L2GenesisGraniteTimeOffset = test.graniteTime
dp.DeployConfig.L2GenesisHoloceneTimeOffset = test.holoceneTime
dp.DeployConfig.L2GenesisIsthmusTimeOffset = test.isthmusTime

if test.canyonTime != nil {
require.Zero(t, uint64(*test.canyonTime)%uint64(dp.DeployConfig.L2BlockTime), "canyon fork must be aligned")
Expand Down
9 changes: 9 additions & 0 deletions op-e2e/actions/upgrades/helpers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ func ApplyDeltaTimeOffset(dp *e2eutils.DeployParams, deltaTimeOffset *hexutil.Ui
dp.DeployConfig.L2GenesisHoloceneTimeOffset = deltaTimeOffset
}
}

// configure Isthmus to not be before Delta accidentally
if dp.DeployConfig.L2GenesisIsthmusTimeOffset != nil {
if deltaTimeOffset == nil {
dp.DeployConfig.L2GenesisIsthmusTimeOffset = nil
} else if *dp.DeployConfig.L2GenesisIsthmusTimeOffset < *deltaTimeOffset {
dp.DeployConfig.L2GenesisIsthmusTimeOffset = deltaTimeOffset
}
}
}
Binary file added op-e2e/config/allocs-l2-isthmus.json.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions op-e2e/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func init() {

configPath := path.Join(root, "op-e2e", "config")
forks := []genesis.L2AllocsMode{
genesis.L2AllocsIsthmus,
genesis.L2AllocsHolocene,
genesis.L2AllocsGranite,
genesis.L2AllocsFjord,
Expand Down Expand Up @@ -244,6 +245,7 @@ func initAllocType(root string, allocType AllocType) {
lgr := log.New()

allocModes := []genesis.L2AllocsMode{
genesis.L2AllocsIsthmus,
genesis.L2AllocsHolocene,
genesis.L2AllocsGranite,
genesis.L2AllocsFjord,
Expand Down Expand Up @@ -287,6 +289,7 @@ func initAllocType(root string, allocType AllocType) {
"l2GenesisFjordTimeOffset": nil,
"l2GenesisGraniteTimeOffset": nil,
"l2GenesisHoloceneTimeOffset": nil,
"l2GenesisIsthmusTimeOffset": nil,
}

upgradeSchedule := new(genesis.UpgradeScheduleDeployConfig)
Expand Down
8 changes: 8 additions & 0 deletions op-e2e/e2eutils/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ func Ether(v uint64) *big.Int {
}

func GetL2AllocsMode(dc *genesis.DeployConfig, t uint64) genesis.L2AllocsMode {
if fork := dc.IsthmusTime(t); fork != nil && *fork <= 0 {
return genesis.L2AllocsIsthmus
}
if fork := dc.HoloceneTime(t); fork != nil && *fork <= 0 {
return genesis.L2AllocsHolocene
}
Expand Down Expand Up @@ -205,6 +208,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) *
FjordTime: deployConf.FjordTime(uint64(deployConf.L1GenesisBlockTimestamp)),
GraniteTime: deployConf.GraniteTime(uint64(deployConf.L1GenesisBlockTimestamp)),
HoloceneTime: deployConf.HoloceneTime(uint64(deployConf.L1GenesisBlockTimestamp)),
IsthmusTime: deployConf.IsthmusTime(uint64(deployConf.L1GenesisBlockTimestamp)),
InteropTime: deployConf.InteropTime(uint64(deployConf.L1GenesisBlockTimestamp)),
AltDAConfig: pcfg,
}
Expand Down Expand Up @@ -235,6 +239,7 @@ func SystemConfigFromDeployConfig(deployConfig *genesis.DeployConfig) eth.System
}

func ApplyDeployConfigForks(deployConfig *genesis.DeployConfig) {
isIsthmus := os.Getenv("OP_E2E_USE_ISTHMUS") == "true"
isHolocene := os.Getenv("OP_E2E_USE_HOLOCENE") == "true"
isGranite := isHolocene || os.Getenv("OP_E2E_USE_GRANITE") == "true"
isFjord := isGranite || os.Getenv("OP_E2E_USE_FJORD") == "true"
Expand All @@ -255,6 +260,9 @@ func ApplyDeployConfigForks(deployConfig *genesis.DeployConfig) {
if isHolocene {
deployConfig.L2GenesisHoloceneTimeOffset = new(hexutil.Uint64)
}
if isIsthmus {
deployConfig.L2GenesisIsthmusTimeOffset = new(hexutil.Uint64)
}
// Canyon and lower is activated by default
deployConfig.L2GenesisCanyonTimeOffset = new(hexutil.Uint64)
deployConfig.L2GenesisRegolithTimeOffset = new(hexutil.Uint64)
Expand Down
23 changes: 22 additions & 1 deletion op-e2e/opgeth/op_geth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -104,7 +106,11 @@ func NewOpGeth(t testing.TB, ctx context.Context, cfg *e2esys.SystemConfig) (*Op
require.NoError(t, err)

// Note: Using CanyonTime here because for OP Stack chains, Shanghai must be activated at the same time as Canyon.
genesisPayload, err := eth.BlockAsPayload(l2GenesisBlock, cfg.DeployConfig.CanyonTime(l2GenesisBlock.Time()))
chainCfg := params.ChainConfig{
CanyonTime: cfg.DeployConfig.CanyonTime(l2GenesisBlock.Time()),
}

genesisPayload, err := eth.BlockAsPayload(l2GenesisBlock, &chainCfg)

require.NoError(t, err)
return &OpGeth{
Expand Down Expand Up @@ -150,6 +156,21 @@ func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*et
return nil, errors.New("required transactions were not included")
}

// if we are at Isthmus, set the withdrawalsRoot in the execution payload to the storage root of the message passer contract
if d.L2ChainConfig.IsIsthmus(uint64(payload.Timestamp)) {
var getProofResponse *eth.AccountResult
rpcClient := d.l2Engine.RPC
err := rpcClient.CallContext(ctx, &getProofResponse, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, payload.BlockHash.String())
if err != nil {
return nil, err
}
if getProofResponse == nil {
return nil, ethereum.NotFound
}
storageHash := getProofResponse.StorageHash
payload.WithdrawalsRoot = &storageHash
}

status, err := d.l2Engine.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot)
if err != nil {
return nil, fmt.Errorf("new payload: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions op-e2e/system/conductor/system_adminrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func TestPostUnsafePayload(t *testing.T) {

blockNumberOne, err := l2Seq.BlockByNumber(ctx, big.NewInt(1))
require.NoError(t, err)
payloadEnv, err := eth.BlockAsPayloadEnv(blockNumberOne, sys.RollupConfig.CanyonTime)
payloadEnv, err := eth.BlockAsPayloadEnv(blockNumberOne, sys.L2GenesisCfg.Config)
require.NoError(t, err)
err = rollupClient.PostUnsafePayload(ctx, payloadEnv)
require.NoError(t, err)
Expand All @@ -212,7 +212,7 @@ func TestPostUnsafePayload(t *testing.T) {
// Test validation
blockNumberTwo, err := l2Seq.BlockByNumber(ctx, big.NewInt(2))
require.NoError(t, err)
payloadEnv, err = eth.BlockAsPayloadEnv(blockNumberTwo, sys.RollupConfig.CanyonTime)
payloadEnv, err = eth.BlockAsPayloadEnv(blockNumberTwo, sys.L2GenesisCfg.Config)
require.NoError(t, err)
payloadEnv.ExecutionPayload.BlockHash = common.Hash{0xaa}
err = rollupClient.PostUnsafePayload(ctx, payloadEnv)
Expand Down
2 changes: 2 additions & 0 deletions op-node/node/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et
return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err)
}

// OutputV0AtBlock uses the WithdrawalsRoot in the block header as the value for the
// output MessagePasserStorageRoot, if Isthmus hard fork has activated.
output, err := n.client.OutputV0AtBlock(ctx, ref.Hash)
if err != nil {
return nil, fmt.Errorf("failed to get L2 output at block %s: %w", ref, err)
Expand Down
42 changes: 39 additions & 3 deletions op-node/p2p/gossip.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,19 @@ func blocksTopicV3(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/2/blocks", cfg.L2ChainID.String())
}

func blocksTopicV4(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/3/blocks", cfg.L2ChainID.String())
}

// BuildSubscriptionFilter builds a simple subscription filter,
// to help protect against peers spamming useless subscriptions.
func BuildSubscriptionFilter(cfg *rollup.Config) pubsub.SubscriptionFilter {
return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg), blocksTopicV3(cfg)) // add more topics here in the future, if any.
return pubsub.NewAllowlistSubscriptionFilter(
blocksTopicV1(cfg),
blocksTopicV2(cfg),
blocksTopicV3(cfg),
blocksTopicV4(cfg), // add more topics here in the future, if any.
)
}

var msgBufPool = sync.Pool{New: func() any {
Expand Down Expand Up @@ -386,6 +395,11 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
return pubsub.ValidationReject
}

if blockVersion.HasWithdrawalsRoot() && payload.WithdrawalsRoot == nil {
log.Warn("payload is on v4 topic, but has nil withdrawals root", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}

seen, ok := blockHeightLRU.Get(uint64(payload.BlockNumber))
if !ok {
seen = new(seenBlocks)
Expand Down Expand Up @@ -450,6 +464,7 @@ type GossipTopicInfo interface {
BlocksTopicV1Peers() []peer.ID
BlocksTopicV2Peers() []peer.ID
BlocksTopicV3Peers() []peer.ID
BlocksTopicV4Peers() []peer.ID
}

type GossipOut interface {
Expand Down Expand Up @@ -485,6 +500,7 @@ type publisher struct {
blocksV1 *blockTopic
blocksV2 *blockTopic
blocksV3 *blockTopic
blocksV4 *blockTopic

runCfg GossipRuntimeConfig
}
Expand All @@ -507,7 +523,12 @@ func combinePeers(allPeers ...[]peer.ID) []peer.ID {
}

func (p *publisher) AllBlockTopicsPeers() []peer.ID {
return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers(), p.BlocksTopicV3Peers())
return combinePeers(
p.BlocksTopicV1Peers(),
p.BlocksTopicV2Peers(),
p.BlocksTopicV3Peers(),
p.BlocksTopicV4Peers(),
)
}

func (p *publisher) BlocksTopicV1Peers() []peer.ID {
Expand All @@ -522,6 +543,10 @@ func (p *publisher) BlocksTopicV3Peers() []peer.ID {
return p.blocksV3.topic.ListPeers()
}

func (p *publisher) BlocksTopicV4Peers() []peer.ID {
return p.blocksV4.topic.ListPeers()
}

func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error {
res := msgBufPool.Get().(*[]byte)
buf := bytes.NewBuffer((*res)[:0])
Expand Down Expand Up @@ -554,7 +579,9 @@ func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.Executio
// This also copies the data, freeing up the original buffer to go back into the pool
out := snappy.Encode(nil, data)

if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) {
if p.cfg.IsIsthmus(uint64(envelope.ExecutionPayload.Timestamp)) {
return p.blocksV4.topic.Publish(ctx, out)
} else if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) {
return p.blocksV3.topic.Publish(ctx, out)
} else if p.cfg.IsCanyon(uint64(envelope.ExecutionPayload.Timestamp)) {
return p.blocksV2.topic.Publish(ctx, out)
Expand Down Expand Up @@ -597,13 +624,22 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con
return nil, fmt.Errorf("failed to setup blocks v3 p2p: %w", err)
}

v4Logger := log.New("topic", "blocksV4")
blocksV4Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv4", v4Logger, BuildBlocksValidator(v4Logger, cfg, runCfg, eth.BlockV4)))
blocksV4, err := newBlockTopic(p2pCtx, blocksTopicV4(cfg), ps, v4Logger, gossipIn, blocksV4Validator)
if err != nil {
p2pCancel()
return nil, fmt.Errorf("failed to setup blocks v4 p2p: %w", err)
}

return &publisher{
log: log,
cfg: cfg,
p2pCancel: p2pCancel,
blocksV1: blocksV1,
blocksV2: blocksV2,
blocksV3: blocksV3,
blocksV4: blocksV4,
runCfg: runCfg,
}, nil
}
Expand Down
2 changes: 2 additions & 0 deletions op-node/p2p/rpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ type PeerStats struct {
BlocksTopic uint `json:"blocksTopic"`
BlocksTopicV2 uint `json:"blocksTopicV2"`
BlocksTopicV3 uint `json:"blocksTopicV3"`
BlocksTopicV4 uint `json:"blocksTopicV4"`
Banned uint `json:"banned"`
Known uint `json:"known"`
}
Expand All @@ -223,6 +224,7 @@ func (s *APIBackend) PeerStats(_ context.Context) (*PeerStats, error) {
BlocksTopic: uint(len(s.node.GossipOut().BlocksTopicV1Peers())),
BlocksTopicV2: uint(len(s.node.GossipOut().BlocksTopicV2Peers())),
BlocksTopicV3: uint(len(s.node.GossipOut().BlocksTopicV3Peers())),
BlocksTopicV4: uint(len(s.node.GossipOut().BlocksTopicV4Peers())),
Banned: 0,
Known: uint(len(pstore.Peers())),
}
Expand Down
Loading