forked from ethereum-optimism/optimism
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
op-chain-ops/devkeys: util for organizing and deriving development ke…
…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
1 parent
1372072
commit 518359c
Showing
3 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
|
||
} |