Skip to content

Commit

Permalink
op-chain-ops/devkeys: util for organizing and deriving development ke…
Browse files Browse the repository at this point in the history
…ys (ethereum-optimism#11578)

* op-chain-ops/devkeys: util for organizing and deriving development keys

* op-chain-ops: simplify devkey domains, and adjust role names, add DependencySetManager

* op-chain-ops: devkeys v2
  • Loading branch information
protolambda authored Aug 23, 2024
1 parent 1372072 commit 518359c
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 0 deletions.
232 changes: 232 additions & 0 deletions op-chain-ops/devkeys/devkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package devkeys

import (
"crypto/ecdsa"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
)

// UserKey identifies an account for any user, by index, not specific to any chain.
type UserKey uint64

const (
DefaultKey UserKey = 0
)

var _ Key = DefaultKey

func (k UserKey) HDPath() string {
return fmt.Sprintf("m/44'/60'/0'/0/%d", uint64(k))
}

func (k UserKey) String() string {
return fmt.Sprintf("user-key-%d", uint64(k))
}

// ChainUserKey is a user-key, but purpose-specific to a single chain.
// ChainID == 0 results in deriving the same key as a regular UserKey for any chain.
type ChainUserKey struct {
ChainID *big.Int
Index uint64
}

var _ Key = ChainUserKey{}

func (k ChainUserKey) HDPath() string {
return fmt.Sprintf("m/44'/60'/0'/%d/%d", k.ChainID, k.Index)
}

func (k ChainUserKey) String() string {
return fmt.Sprintf("user-key-chain(%d)-%d", k.ChainID, k.Index)
}

// ChainUserKeys is a helper method to not repeat chainID for every user key
func ChainUserKeys(chainID *big.Int) func(index uint64) ChainUserKey {
return func(index uint64) ChainUserKey {
return ChainUserKey{ChainID: chainID, Index: index}
}
}

// SuperchainOperatorRole identifies an account used in the operations of superchain contracts
type SuperchainOperatorRole uint64

const (
// SuperchainDeployerKey is the deployer of the superchain contracts.
SuperchainDeployerKey SuperchainOperatorRole = 0
// SuperchainConfigGuardianKey is the Guardian of the SuperchainConfig.
SuperchainConfigGuardianKey SuperchainOperatorRole = 1
// DependencySetManagerKey is the key used to manage the dependency set of a superchain.
DependencySetManagerKey SuperchainOperatorRole = 2
)

func (role SuperchainOperatorRole) String() string {
switch role {
case SuperchainDeployerKey:
return "superchain-deployer"
case SuperchainConfigGuardianKey:
return "superchain-config-guardian"
case DependencySetManagerKey:
return "dependency-set-manager"
default:
return fmt.Sprintf("unknown-superchain-%d", uint64(role))
}
}

// SuperchainOperatorKey is an account specific to an OperationRole of a given OP-Stack chain.
type SuperchainOperatorKey struct {
ChainID *big.Int
Role SuperchainOperatorRole
}

var _ Key = SuperchainOperatorKey{}

func (k SuperchainOperatorKey) HDPath() string {
return fmt.Sprintf("m/44'/60'/1'/%d/%d", k.ChainID, uint64(k.Role))
}

func (k SuperchainOperatorKey) String() string {
return fmt.Sprintf("superchain(%d)-%s", k.ChainID, k.Role)
}

// SuperchainOperatorKeys is a helper method to not repeat chainID on every operator role
func SuperchainOperatorKeys(chainID *big.Int) func(role SuperchainOperatorRole) SuperchainOperatorKey {
return func(role SuperchainOperatorRole) SuperchainOperatorKey {
return SuperchainOperatorKey{ChainID: chainID, Role: role}
}
}

// ChainOperatorRole identifies an account for a specific OP-Stack chain operator role.
type ChainOperatorRole uint64

const (
// DeployerRole is the deployer of contracts for an OP-Stack chain
DeployerRole ChainOperatorRole = 0
// ProposerRole is the key used by op-proposer
ProposerRole ChainOperatorRole = 1
// BatcherRole is the key used by op-batcher
BatcherRole ChainOperatorRole = 2
// SequencerP2PRole is the key used to publish sequenced L2 blocks
SequencerP2PRole ChainOperatorRole = 3
// ChallengerRole is the key used by op-challenger
ChallengerRole ChainOperatorRole = 4
// L2ProxyAdminOwnerRole is the key that controls the ProxyAdmin predeploy in L2
L2ProxyAdminOwnerRole ChainOperatorRole = 5
// L1ProxyAdminOwnerRole is the key that owns the ProxyAdmin on the L1 side of the deployment.
// This can be the ProxyAdmin of a L2 chain deployment, or a superchain deployment, depending on the domain.
L1ProxyAdminOwnerRole ChainOperatorRole = 6
// BaseFeeVaultRecipientRole is the key that receives from the BaseFeeVault predeploy
BaseFeeVaultRecipientRole ChainOperatorRole = 7
// L1FeeVaultRecipientRole is the key that receives from the L1FeeVault predeploy
L1FeeVaultRecipientRole ChainOperatorRole = 8
// SequencerFeeVaultRecipientRole is the key that receives form the SequencerFeeVault predeploy
SequencerFeeVaultRecipientRole ChainOperatorRole = 9
)

func (role ChainOperatorRole) String() string {
switch role {
case DeployerRole:
return "deployer"
case ProposerRole:
return "proposer"
case BatcherRole:
return "batcher"
case SequencerP2PRole:
return "sequencer-p2p"
case ChallengerRole:
return "challenger"
case L2ProxyAdminOwnerRole:
return "l2-proxy-admin-owner"
case L1ProxyAdminOwnerRole:
return "l1-proxy-admin-owner"
case BaseFeeVaultRecipientRole:
return "base-fee-vault-recipient"
case L1FeeVaultRecipientRole:
return "l1-fee-vault-recipient"
case SequencerFeeVaultRecipientRole:
return "sequencer-fee-vault-recipient"
default:
return fmt.Sprintf("unknown-operator-%d", uint64(role))
}
}

func (role ChainOperatorRole) Key(chainID *big.Int) *ChainOperatorKey {
return &ChainOperatorKey{
ChainID: chainID,
Role: role,
}
}

// ChainOperatorKey is an account specific to an OperationRole of a given OP-Stack chain.
type ChainOperatorKey struct {
ChainID *big.Int
Role ChainOperatorRole
}

var _ Key = ChainOperatorKey{}

func (k ChainOperatorKey) HDPath() string {
return fmt.Sprintf("m/44'/60'/2'/%d/%d", k.ChainID, uint64(k.Role))
}

func (k ChainOperatorKey) String() string {
return fmt.Sprintf("chain(%d)-%s", k.ChainID, k.Role)
}

// ChainOperatorKeys is a helper method to not repeat chainID on every operator role
func ChainOperatorKeys(chainID *big.Int) func(ChainOperatorRole) ChainOperatorKey {
return func(role ChainOperatorRole) ChainOperatorKey {
return ChainOperatorKey{ChainID: chainID, Role: role}
}
}

// Key identifies an account, and produces an HD-Path to derive the secret-key from.
//
// We organize the dev keys with a mnemonic key-path structure as following:
// BIP-44: `m / purpose' / coin_type' / account' / change / address_index`
// purpose = standard secp256k1 usage (Eth2 BLS keys use different purpose data).
// coin_type = chain type, set to 60' for ETH. See SLIP-0044.
// account = for different identities, used here to separate domains:
//
// domain 0: users
// domain 1: superchain operations
// domain 2: chain operations
//
// change = to separate external and internal addresses.
//
// Used here for chain ID, may be 0 for user accounts (any-chain addresses).
//
// address_index = used here to separate roles.
// The `'` char signifies BIP-32 hardened derivation.
//
// See:
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
// https://github.com/satoshilabs/slips/blob/master/slip-0044.md
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
type Key interface {
// HDPath produces the hierarchical derivation path to (re)create this key.
HDPath() string
// String describes the role of the key
String() string
}

// Secrets selects a secret-key based on a key.
// This is meant for dev-purposes only.
// Secret keys should not directly be exposed to live production services.
type Secrets interface {
Secret(key Key) (*ecdsa.PrivateKey, error)
}

// Addresses selects an address based on a key.
// This interface is preferred in tools that do not directly rely on secret-key material.
type Addresses interface {
// Address produces an address for the given key
Address(key Key) (common.Address, error)
}

// Keys is a joint interface of Secrets and Addresses
type Keys interface {
Secrets
Addresses
}
47 changes: 47 additions & 0 deletions op-chain-ops/devkeys/hd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package devkeys

import (
"crypto/ecdsa"
"fmt"

hdwallet "github.com/ethereum-optimism/go-ethereum-hdwallet"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

const TestMnemonic = "test test test test test test test test test test test junk"

type MnemonicDevKeys struct {
w *hdwallet.Wallet
}

var _ Keys = (*MnemonicDevKeys)(nil)

func NewMnemonicDevKeys(mnemonic string) (*MnemonicDevKeys, error) {
w, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil {
return nil, fmt.Errorf("invalid mnemonic: %w", err)
}
return &MnemonicDevKeys{w: w}, nil
}

func (d *MnemonicDevKeys) Secret(key Key) (*ecdsa.PrivateKey, error) {
account := accounts.Account{URL: accounts.URL{
Path: key.HDPath(),
}}
priv, err := d.w.PrivateKey(account)
if err != nil {
return nil, fmt.Errorf("failed to derive key of path %s (key description: %s): %w", account.URL.Path, key.String(), err)
}
return priv, nil
}

func (d *MnemonicDevKeys) Address(key Key) (common.Address, error) {
secret, err := d.Secret(key)
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(secret.PublicKey), nil
}
68 changes: 68 additions & 0 deletions op-chain-ops/devkeys/hd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package devkeys

import (
"math/big"
"testing"

"github.com/stretchr/testify/require"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

func TestMnemonicDevKeys(t *testing.T) {
m, err := NewMnemonicDevKeys(TestMnemonic)
require.NoError(t, err)

t.Run("default", func(t *testing.T) {
defaultAccount, err := m.Address(DefaultKey)
require.NoError(t, err)
// Sanity check against a well-known dev account address,
// to ensure the mnemonic path is formatted with the right hardening at each path segment.
require.Equal(t, common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), defaultAccount)

// Check that we can localize users to a chain
chain1UserKey0, err := m.Address(ChainUserKeys(big.NewInt(1))(0))
require.NoError(t, err)
require.NotEqual(t, defaultAccount, chain1UserKey0)
})

t.Run("superchain-operator", func(t *testing.T) {
keys := SuperchainOperatorKeys(big.NewInt(1))
// Check that each key address and name is unique
addrs := make(map[common.Address]struct{})
names := make(map[string]struct{})
for i := SuperchainOperatorRole(0); i < 20; i++ {
key := keys(i)
secret, err := m.Secret(key)
require.NoError(t, err)
addr, err := m.Address(key)
require.NoError(t, err)
require.Equal(t, crypto.PubkeyToAddress(secret.PublicKey), addr)
addrs[addr] = struct{}{}
names[key.String()] = struct{}{}
}
require.Len(t, addrs, 20, "unique address for each account")
require.Len(t, names, 20, "unique name for each account")
})

t.Run("chain-operator", func(t *testing.T) {
keys := ChainOperatorKeys(big.NewInt(1))
// Check that each key address and name is unique
addrs := make(map[common.Address]struct{})
names := make(map[string]struct{})
for i := ChainOperatorRole(0); i < 20; i++ {
key := keys(i)
secret, err := m.Secret(key)
require.NoError(t, err)
addr, err := m.Address(key)
require.NoError(t, err)
require.Equal(t, crypto.PubkeyToAddress(secret.PublicKey), addr)
addrs[addr] = struct{}{}
names[key.String()] = struct{}{}
}
require.Len(t, addrs, 20, "unique address for each account")
require.Len(t, names, 20, "unique name for each account")
})

}

0 comments on commit 518359c

Please sign in to comment.