What keys are managed?
There is often a function in the underlying components of a blockchain application that requires transactions to be sent into the blockchain on a continuous basis, such as arbitrum'sSequencerneed to continuously send blocks of L2.stark The chainlink needs to send datafeed transactions at regular intervals for single-step proof/rBlock release transactions. Each of these transactions needs to be signed by an account on L1, and how to securely use and manage this key is of interest.
reach a verdict
From what I've seen there are generally two ways:
- Configure private key via configuration file
- Use the filekey method:
- Note that file generally requires a password, which is entered in the terminal console after startup
Of course, key management is not just about simply injecting keys into a program, but how to use those keys securely inside the program; after all, if the keys are sent in an interface that may be called by an external interface, it may reduce the security of the keys.
Key security level (in decreasing order, considering only cases where the encryption algorithm is publicly available):
- Hackers can't know any plaintext & ciphertext
- Hackers can get the ciphertext
- Hackers can get the plaintext that corresponds to the ciphertext.
- Hackers can construct their own plaintext to generate ciphertext.
So the key needs to be protected in the program as well.
In Ethernet, the address of the private key is restored to 0 every time the private key is used, just to avoid the private key leaking in memory.
The principle of private key leakage is roughly that the geth program will release the memory after playing with it, and it will not set all the memory values to 0, but just tell the operating system, "I don't need this memory anymore, you can allocate it to another program". After the other program applies for this memory, it can read the value of this memory directly (the classic case is that if you initialize a variable in C without assigning a value to it, its value is not 0, but the value of the memory in which the value was originally).
Arbitrum's treatment program
Let's start by looking at the bottom level calls
As you can see in the single-step proof call, the user information for this transaction is stored in the auth field
func (m *ChallengeManager) IssueOneStepProof(
ctx ,
oldState *ChallengeState,
startSegment int,
) (*, error) {
position := [startSegment].Position
proof, err := (ctx, position)
if err != nil {
return nil, ("error getting OSP from challenge %v backend at step %v: %w", , position, err)
}
return (
, // User information is stored in this field
,
{
OldSegmentsStart: ,
OldSegmentsLength: new().Sub(, ),
OldSegments: ,
ChallengePosition: (int64(startSegment)),
},
proof,
)
}
You can continue to click in to see exactly how it is used, ultimately it is auth that contains a variable (of function type) from which the signature is made, (the lifecycle of this function that we need to find, which is the lifecycle of the key).
So moving on, look at the constructor for this challengeManager
func NewChallengeManager(
ctx ,
l1client ,
auth *,
fromAddr ,
challengeManagerAddr ,
challengeIndex uint64,
val *StatelessBlockValidator,
startL1Block uint64,
confirmationBlocks int64,
) (*ChallengeManager, error) {
...
return &ChallengeManager{
challengeCore: &challengeCore{
con: con,
challengeManagerAddr: challengeManagerAddr,
challengeIndex: challengeIndex,
client: l1client,
auth: auth, // That's what's up there.auth
actingAs: fromAddr,
startL1Block: new().SetUint64(startL1Block),
confirmationBlocks: confirmationBlocks,
},
blockChallengeBackend: backend,
validator: val,
wasmModuleRoot: ,
maxBatchesRead: ,
}, nil
}
You can see that auth is passed from above
Continuing from the top, auth comes from a structure called Builder, and it's a good thing that the constructor for this structure is only called once (we're assuming that the only constructor we get is the auth we're looking for, and that it hasn't changed in the meantime).
func NewBuilder(wallet ValidatorWalletInterface) (*Builder, error) {
randKey, err := ()
if err != nil {
return nil, err
}
builderAuth := ()
var isAuthFake bool
if builderAuth == nil {
// Make a fake auth so we have txs to give to the smart contract wallet
builderAuth, err = (randKey, (9999999))
if err != nil {
return nil, err
}
isAuthFake = true
}
return &Builder{
builderAuth: builderAuth,
wallet: wallet,
L1Interface: wallet.L1Client(),
isAuthFake: isAuthFake,
}, nil
}
There are two routes to builder's auth, AuthIfEoa, which parses the private key from eoa, and generating your own private key.
So the key is with what is the wallet here (i.e. now moving from tracking auth to tracking wallet)
It was eventually discovered that the wallet was constructed inside the createNoteImpl method of the
var wallet = (l1client, )
if !(, "watchtower") {
if || (txOptsValidator == nil && == "") {// contract account
var existingWalletAddress *
if len() > 0 {
if !() {
("invalid validator smart contract wallet", "addr", )
return nil, ("invalid validator smart contract wallet address")
}
tmpAddress := ()
existingWalletAddress = &tmpAddress
}
wallet, err = (dp, existingWalletAddress, , , l1Reader, txOptsValidator, int64(), func() {}, getExtraGas)
if err != nil {
return nil, err
}
} else {
if len() > 0 {
return nil, ("validator contract wallet specified but flag to use a smart contract wallet was not specified")
}
wallet, err = (dp, , l1client, getExtraGas)
if err != nil {
return nil, err
}
}
}
Continuing the trace we get that the validation method in the wellet is provided by txOptsValidator
Moving up to continue looking for txOptsValidator
Most heavily found mainImpl
if sequencerNeedsKey || {
l1TransactionOptsBatchPoster, dataSigner, err = ("l1-batch-poster", &, new().SetUint64())
if err != nil {
()
("error opening Batch poster parent chain wallet", "path", , "account", , "err", err)
}
if {
return 0
}
}
if validatorNeedsKey || {
l1TransactionOptsValidator, _, err = ("l1-validator", &, new().SetUint64())
if err != nil {
()
("error opening Validator parent chain wallet", "path", , "account", , "err", err)
}
if {
return 0
}
}
We get l1TransactionOptsValidator by using the This configuration item gets.
The final data structure looks like this
type WalletConfig struct {
Pathname string `koanf:"pathname"`
Password string `koanf:"password"`
PrivateKey string `koanf:"private-key"`
Account string `koanf:"account"`
OnlyCreateKey bool `koanf:"only-create-key"`
}
Go ahead and click on OpenWallet to see how he handles these configuration items
In the case of a private key it will eventually go to this method
func NewKeyedTransactorWithChainID(key *, chainID *) (*TransactOpts, error) {
keyAddr := ()
if chainID == nil {
return nil, ErrNoChainID
}
signer := (chainID)
return &TransactOpts{
From: keyAddr,
Signer: func(address , tx *) (*, error) { // signerIt's the signature method we've been looking for to use when sending transactions.
if address != keyAddr {
return nil, ErrNotAuthorized
}
signature, err := ((tx).Bytes(), key)
if err != nil {
return nil, err
}
return (signer, signature)
},
Context: (),
}, nil
}
As you can see here, the private key is always stored in the signer method, and there is no passing of the private key as a parameter throughout its use.
If you are using filekey+password you will go to this method
func NewKeyStoreTransactor(keystore *, account ) (*TransactOpts, error) {
("WARNING: NewKeyStoreTransactor has been deprecated in favour of NewTransactorWithChainID")
signer := {}
return &TransactOpts{
From: ,
Signer: func(address , tx *) (*, error) {
if address != {
return nil, ErrNotAuthorized
}
signature, err := (account, (tx).Bytes())
if err != nil {
return nil, err
}
return (signer, signature)
},
Context: (),
}, nil
}
The program will construct a keystore based on the filekey, and subsequent signatures will be signed in the keystore.
Note that the password for filekey is entered in the terminal console, where the readPass function is as follows
func readPass() (string, error) {
bytePassword, err := ()
if err != nil {
return "", err
}
passphrase := string(bytePassword)
passphrase = (passphrase)
return passphrase, nil
}