all: remove personal
RPC namespace (#30704)
This PR is a first step towards removing account management from geth, and contains a lot of the user-facing changes. With this PR, the `personal` namespace disappears. **Note**: `personal` namespace has been deprecated for quite some time (since https://github.com/ethereum/go-ethereum/pull/26390 1 year and 8 months ago), and users who have wanted to use it has been forced to used the flag `--rpc.enabledeprecatedpersonal`. So I think it's fairly non-controversial to drop it at this point. Specifically, this means: - Account/wallet listing -`personal.getListAccounts` -`personal.listAccounts` -`personal.getListWallets` -`personal.listWallets` - Lock/unlock -`personal.lockAccount` -`personal.openWallet` -`personal.unlockAccount` - Sign ops -`personal.sign` -`personal.sendTransaction` -`personal.signTransaction` - Imports / inits -`personal.deriveAccount` -`personal.importRawKey` -`personal.initializeWallet` -`personal.newAccount` -`personal.unpair` - Other: -`personal.ecRecover` The underlying keystores and account managent code is still in place, which means that `geth --dev` still works as expected, so that e.g. the example below still works: ``` > eth.sendTransaction({data:"0x6060", value: 1, from:eth.accounts[0]}) ``` Also, `ethkey` and `clef` are untouched. With the removal of `personal`, as far as I know we have no more API methods which contain credentials, and if we want to implement logging-capabilities of RPC ingress payload, it would be possible after this. --------- Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
parent
a1d049c1c4
commit
f3b4bbbaf3
@ -67,7 +67,7 @@ var (
|
||||
utils.SmartCardDaemonPathFlag,
|
||||
utils.OverrideCancun,
|
||||
utils.OverrideVerkle,
|
||||
utils.EnablePersonal,
|
||||
utils.EnablePersonal, // deprecated
|
||||
utils.TxPoolLocalsFlag,
|
||||
utils.TxPoolNoLocalsFlag,
|
||||
utils.TxPoolJournalFlag,
|
||||
|
@ -734,11 +734,6 @@ var (
|
||||
Value: node.DefaultConfig.BatchResponseMaxSize,
|
||||
Category: flags.APICategory,
|
||||
}
|
||||
EnablePersonal = &cli.BoolFlag{
|
||||
Name: "rpc.enabledeprecatedpersonal",
|
||||
Usage: "Enables the (deprecated) personal namespace",
|
||||
Category: flags.APICategory,
|
||||
}
|
||||
|
||||
// Network Settings
|
||||
MaxPeersFlag = &cli.IntFlag{
|
||||
@ -1392,9 +1387,8 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
|
||||
if ctx.IsSet(JWTSecretFlag.Name) {
|
||||
cfg.JWTSecret = ctx.String(JWTSecretFlag.Name)
|
||||
}
|
||||
|
||||
if ctx.IsSet(EnablePersonal.Name) {
|
||||
cfg.EnablePersonal = true
|
||||
log.Warn(fmt.Sprintf("Option --%s is deprecated. The 'personal' RPC namespace has been removed.", EnablePersonal.Name))
|
||||
}
|
||||
|
||||
if ctx.IsSet(ExternalSignerFlag.Name) {
|
||||
|
@ -153,6 +153,12 @@ var (
|
||||
Usage: "Enable expensive metrics collection and reporting (deprecated)",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
// Deprecated Oct 2024
|
||||
EnablePersonal = &cli.BoolFlag{
|
||||
Name: "rpc.enabledeprecatedpersonal",
|
||||
Usage: "This used to enable the 'personal' namespace.",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
)
|
||||
|
||||
// showDeprecated displays deprecated flags that will be soon removed from the codebase.
|
||||
|
@ -19,15 +19,12 @@ package console
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
||||
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/console/prompt"
|
||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
||||
@ -51,268 +48,6 @@ func newBridge(client *rpc.Client, prompter prompt.UserPrompter, printer io.Writ
|
||||
}
|
||||
}
|
||||
|
||||
func getJeth(vm *goja.Runtime) *goja.Object {
|
||||
jeth := vm.Get("jeth")
|
||||
if jeth == nil {
|
||||
panic(vm.ToValue("jeth object does not exist"))
|
||||
}
|
||||
return jeth.ToObject(vm)
|
||||
}
|
||||
|
||||
// NewAccount is a wrapper around the personal.newAccount RPC method that uses a
|
||||
// non-echoing password prompt to acquire the passphrase and executes the original
|
||||
// RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
|
||||
func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
|
||||
var (
|
||||
password string
|
||||
confirm string
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
// No password was specified, prompt the user for it
|
||||
case len(call.Arguments) == 0:
|
||||
if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if password != confirm {
|
||||
return nil, errors.New("passwords don't match")
|
||||
}
|
||||
// A single string password was specified, use that
|
||||
case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
|
||||
password = call.Argument(0).ToString().String()
|
||||
default:
|
||||
return nil, errors.New("expected 0 or 1 string argument")
|
||||
}
|
||||
// Password acquired, execute the call and return
|
||||
newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.newAccount is not callable")
|
||||
}
|
||||
ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// OpenWallet is a wrapper around personal.openWallet which can interpret and
|
||||
// react to certain error messages, such as the Trezor PIN matrix request.
|
||||
func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
|
||||
// Make sure we have a wallet specified to open
|
||||
if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
|
||||
return nil, errors.New("first argument must be the wallet URL to open")
|
||||
}
|
||||
wallet := call.Argument(0)
|
||||
|
||||
var passwd goja.Value
|
||||
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
|
||||
passwd = call.VM.ToValue("")
|
||||
} else {
|
||||
passwd = call.Argument(1)
|
||||
}
|
||||
// Open the wallet and return if successful in itself
|
||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.openWallet is not callable")
|
||||
}
|
||||
val, err := openWallet(goja.Null(), wallet, passwd)
|
||||
if err == nil {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Wallet open failed, report error unless it's a PIN or PUK entry
|
||||
switch {
|
||||
case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
|
||||
val, err = b.readPinAndReopenWallet(call)
|
||||
if err == nil {
|
||||
return val, nil
|
||||
}
|
||||
val, err = b.readPassphraseAndReopenWallet(call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
|
||||
// PUK input requested, fetch from the user and call open again
|
||||
input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwd = call.VM.ToValue(input)
|
||||
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
|
||||
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
|
||||
return nil, err
|
||||
}
|
||||
// PIN input requested, fetch from the user and call open again
|
||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
|
||||
// PIN unblock requested, fetch PUK and new PIN from the user
|
||||
var pukpin string
|
||||
input, err := b.prompter.PromptPassword("Please enter current PUK: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pukpin = input
|
||||
input, err = b.prompter.PromptPassword("Please enter new PIN: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pukpin += input
|
||||
|
||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
|
||||
// PIN input requested, fetch from the user and call open again
|
||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown error occurred, drop to the user
|
||||
return nil, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
|
||||
wallet := call.Argument(0)
|
||||
input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.openWallet is not callable")
|
||||
}
|
||||
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
|
||||
}
|
||||
|
||||
func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
|
||||
wallet := call.Argument(0)
|
||||
// Trezor PIN matrix input requested, display the matrix to the user and fetch the data
|
||||
fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
|
||||
fmt.Fprintf(b.printer, "7 | 8 | 9\n")
|
||||
fmt.Fprintf(b.printer, "--+---+--\n")
|
||||
fmt.Fprintf(b.printer, "4 | 5 | 6\n")
|
||||
fmt.Fprintf(b.printer, "--+---+--\n")
|
||||
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
|
||||
|
||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.openWallet is not callable")
|
||||
}
|
||||
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
|
||||
}
|
||||
|
||||
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
|
||||
// uses a non-echoing password prompt to acquire the passphrase and executes the
|
||||
// original RPC method (saved in jeth.unlockAccount) with it to actually execute
|
||||
// the RPC call.
|
||||
func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
|
||||
if len(call.Arguments) < 1 {
|
||||
return nil, errors.New("usage: unlockAccount(account, [ password, duration ])")
|
||||
}
|
||||
|
||||
account := call.Argument(0)
|
||||
// Make sure we have an account specified to unlock.
|
||||
if goja.IsUndefined(account) || goja.IsNull(account) || account.ExportType().Kind() != reflect.String {
|
||||
return nil, errors.New("first argument must be the account to unlock")
|
||||
}
|
||||
|
||||
// If password is not given or is the null value, prompt the user for it.
|
||||
var passwd goja.Value
|
||||
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
|
||||
fmt.Fprintf(b.printer, "Unlock account %s\n", account)
|
||||
input, err := b.prompter.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwd = call.VM.ToValue(input)
|
||||
} else {
|
||||
if call.Argument(1).ExportType().Kind() != reflect.String {
|
||||
return nil, errors.New("password must be a string")
|
||||
}
|
||||
passwd = call.Argument(1)
|
||||
}
|
||||
|
||||
// Third argument is the duration how long the account should be unlocked.
|
||||
duration := goja.Null()
|
||||
if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
|
||||
if !isNumber(call.Argument(2)) {
|
||||
return nil, errors.New("unlock duration must be a number")
|
||||
}
|
||||
duration = call.Argument(2)
|
||||
}
|
||||
|
||||
// Send the request to the backend and return.
|
||||
unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.unlockAccount is not callable")
|
||||
}
|
||||
return unlockAccount(goja.Null(), account, passwd, duration)
|
||||
}
|
||||
|
||||
// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
|
||||
// prompt to acquire the passphrase and executes the original RPC method (saved in
|
||||
// jeth.sign) with it to actually execute the RPC call.
|
||||
func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
|
||||
if nArgs := len(call.Arguments); nArgs < 2 {
|
||||
return nil, errors.New("usage: sign(message, account, [ password ])")
|
||||
}
|
||||
var (
|
||||
message = call.Argument(0)
|
||||
account = call.Argument(1)
|
||||
passwd = call.Argument(2)
|
||||
)
|
||||
|
||||
if goja.IsUndefined(message) || message.ExportType().Kind() != reflect.String {
|
||||
return nil, errors.New("first argument must be the message to sign")
|
||||
}
|
||||
if goja.IsUndefined(account) || account.ExportType().Kind() != reflect.String {
|
||||
return nil, errors.New("second argument must be the account to sign with")
|
||||
}
|
||||
|
||||
// if the password is not given or null ask the user and ensure password is a string
|
||||
if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
|
||||
fmt.Fprintf(b.printer, "Give password for account %s\n", account)
|
||||
input, err := b.prompter.PromptPassword("Password: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwd = call.VM.ToValue(input)
|
||||
} else if passwd.ExportType().Kind() != reflect.String {
|
||||
return nil, errors.New("third argument must be the password to unlock the account")
|
||||
}
|
||||
|
||||
// Send the request to the backend and return
|
||||
sign, callable := goja.AssertFunction(getJeth(call.VM).Get("sign"))
|
||||
if !callable {
|
||||
return nil, errors.New("jeth.sign is not callable")
|
||||
}
|
||||
return sign(goja.Null(), message, account, passwd)
|
||||
}
|
||||
|
||||
// Sleep will block the console for the specified number of seconds.
|
||||
func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
|
||||
if nArgs := len(call.Arguments); nArgs < 1 {
|
||||
|
@ -1,48 +0,0 @@
|
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
||||
)
|
||||
|
||||
// TestUndefinedAsParam ensures that personal functions can receive
|
||||
// `undefined` as a parameter.
|
||||
func TestUndefinedAsParam(t *testing.T) {
|
||||
b := bridge{}
|
||||
call := jsre.Call{}
|
||||
call.Arguments = []goja.Value{goja.Undefined()}
|
||||
|
||||
b.UnlockAccount(call)
|
||||
b.Sign(call)
|
||||
b.Sleep(call)
|
||||
}
|
||||
|
||||
// TestNullAsParam ensures that personal functions can receive
|
||||
// `null` as a parameter.
|
||||
func TestNullAsParam(t *testing.T) {
|
||||
b := bridge{}
|
||||
call := jsre.Call{}
|
||||
call.Arguments = []goja.Value{goja.Null()}
|
||||
|
||||
b.UnlockAccount(call)
|
||||
b.Sign(call)
|
||||
b.Sleep(call)
|
||||
}
|
@ -142,7 +142,6 @@ func (c *Console) init(preload []string) error {
|
||||
// Add bridge overrides for web3.js functionality.
|
||||
c.jsre.Do(func(vm *goja.Runtime) {
|
||||
c.initAdmin(vm, bridge)
|
||||
c.initPersonal(vm, bridge)
|
||||
})
|
||||
|
||||
// Preload JavaScript files.
|
||||
@ -249,30 +248,6 @@ func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
|
||||
}
|
||||
}
|
||||
|
||||
// initPersonal redirects account-related API methods through the bridge.
|
||||
//
|
||||
// If the console is in interactive mode and the 'personal' API is available, override
|
||||
// the openWallet, unlockAccount, newAccount and sign methods since these require user
|
||||
// interaction. The original web3 callbacks are stored in 'jeth'. These will be called
|
||||
// by the bridge after the prompt and send the original web3 request to the backend.
|
||||
func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
|
||||
personal := getObject(vm, "personal")
|
||||
if personal == nil || c.prompter == nil {
|
||||
return
|
||||
}
|
||||
log.Warn("Enabling deprecated personal namespace")
|
||||
jeth := vm.NewObject()
|
||||
vm.Set("jeth", jeth)
|
||||
jeth.Set("openWallet", personal.Get("openWallet"))
|
||||
jeth.Set("unlockAccount", personal.Get("unlockAccount"))
|
||||
jeth.Set("newAccount", personal.Get("newAccount"))
|
||||
jeth.Set("sign", personal.Get("sign"))
|
||||
personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
|
||||
personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
|
||||
personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
|
||||
personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
|
||||
}
|
||||
|
||||
func (c *Console) clearHistory() {
|
||||
c.history = nil
|
||||
c.prompter.ClearHistory()
|
||||
|
1
go.mod
1
go.mod
@ -63,7 +63,6 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/supranational/blst v0.3.13
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
go.uber.org/automaxprocs v1.5.2
|
||||
golang.org/x/crypto v0.22.0
|
||||
|
2
go.sum
2
go.sum
@ -499,8 +499,6 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
|
@ -29,8 +29,6 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
@ -51,7 +49,6 @@ import (
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is
|
||||
@ -298,344 +295,6 @@ func (api *EthereumAccountAPI) Accounts() []common.Address {
|
||||
return api.am.Accounts()
|
||||
}
|
||||
|
||||
// PersonalAccountAPI provides an API to access accounts managed by this node.
|
||||
// It offers methods to create, (un)lock en list accounts. Some methods accept
|
||||
// passwords and are therefore considered private by default.
|
||||
type PersonalAccountAPI struct {
|
||||
am *accounts.Manager
|
||||
nonceLock *AddrLocker
|
||||
b Backend
|
||||
}
|
||||
|
||||
// NewPersonalAccountAPI creates a new PersonalAccountAPI.
|
||||
func NewPersonalAccountAPI(b Backend, nonceLock *AddrLocker) *PersonalAccountAPI {
|
||||
return &PersonalAccountAPI{
|
||||
am: b.AccountManager(),
|
||||
nonceLock: nonceLock,
|
||||
b: b,
|
||||
}
|
||||
}
|
||||
|
||||
// ListAccounts will return a list of addresses for accounts this node manages.
|
||||
func (api *PersonalAccountAPI) ListAccounts() []common.Address {
|
||||
return api.am.Accounts()
|
||||
}
|
||||
|
||||
// rawWallet is a JSON representation of an accounts.Wallet interface, with its
|
||||
// data contents extracted into plain fields.
|
||||
type rawWallet struct {
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
Failure string `json:"failure,omitempty"`
|
||||
Accounts []accounts.Account `json:"accounts,omitempty"`
|
||||
}
|
||||
|
||||
// ListWallets will return a list of wallets this node manages.
|
||||
func (api *PersonalAccountAPI) ListWallets() []rawWallet {
|
||||
wallets := make([]rawWallet, 0) // return [] instead of nil if empty
|
||||
for _, wallet := range api.am.Wallets() {
|
||||
status, failure := wallet.Status()
|
||||
|
||||
raw := rawWallet{
|
||||
URL: wallet.URL().String(),
|
||||
Status: status,
|
||||
Accounts: wallet.Accounts(),
|
||||
}
|
||||
if failure != nil {
|
||||
raw.Failure = failure.Error()
|
||||
}
|
||||
wallets = append(wallets, raw)
|
||||
}
|
||||
return wallets
|
||||
}
|
||||
|
||||
// OpenWallet initiates a hardware wallet opening procedure, establishing a USB
|
||||
// connection and attempting to authenticate via the provided passphrase. Note,
|
||||
// the method may return an extra challenge requiring a second open (e.g. the
|
||||
// Trezor PIN matrix challenge).
|
||||
func (api *PersonalAccountAPI) OpenWallet(url string, passphrase *string) error {
|
||||
wallet, err := api.am.Wallet(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass := ""
|
||||
if passphrase != nil {
|
||||
pass = *passphrase
|
||||
}
|
||||
return wallet.Open(pass)
|
||||
}
|
||||
|
||||
// DeriveAccount requests an HD wallet to derive a new account, optionally pinning
|
||||
// it for later reuse.
|
||||
func (api *PersonalAccountAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) {
|
||||
wallet, err := api.am.Wallet(url)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
derivPath, err := accounts.ParseDerivationPath(path)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
if pin == nil {
|
||||
pin = new(bool)
|
||||
}
|
||||
return wallet.Derive(derivPath, *pin)
|
||||
}
|
||||
|
||||
// NewAccount will create a new account and returns the address for the new account.
|
||||
func (api *PersonalAccountAPI) NewAccount(password string) (common.AddressEIP55, error) {
|
||||
ks, err := fetchKeystore(api.am)
|
||||
if err != nil {
|
||||
return common.AddressEIP55{}, err
|
||||
}
|
||||
acc, err := ks.NewAccount(password)
|
||||
if err == nil {
|
||||
addrEIP55 := common.AddressEIP55(acc.Address)
|
||||
log.Info("Your new key was generated", "address", addrEIP55.String())
|
||||
log.Warn("Please backup your key file!", "path", acc.URL.Path)
|
||||
log.Warn("Please remember your password!")
|
||||
return addrEIP55, nil
|
||||
}
|
||||
return common.AddressEIP55{}, err
|
||||
}
|
||||
|
||||
// fetchKeystore retrieves the encrypted keystore from the account manager.
|
||||
func fetchKeystore(am *accounts.Manager) (*keystore.KeyStore, error) {
|
||||
if ks := am.Backends(keystore.KeyStoreType); len(ks) > 0 {
|
||||
return ks[0].(*keystore.KeyStore), nil
|
||||
}
|
||||
return nil, errors.New("local keystore not used")
|
||||
}
|
||||
|
||||
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (api *PersonalAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) {
|
||||
key, err := crypto.HexToECDSA(privkey)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
ks, err := fetchKeystore(api.am)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
acc, err := ks.ImportECDSA(key, password)
|
||||
return acc.Address, err
|
||||
}
|
||||
|
||||
// UnlockAccount will unlock the account associated with the given address with
|
||||
// the given password for duration seconds. If duration is nil it will use a
|
||||
// default of 300 seconds. It returns an indication if the account was unlocked.
|
||||
func (api *PersonalAccountAPI) UnlockAccount(ctx context.Context, addr common.Address, password string, duration *uint64) (bool, error) {
|
||||
// When the API is exposed by external RPC(http, ws etc), unless the user
|
||||
// explicitly specifies to allow the insecure account unlocking, otherwise
|
||||
// it is disabled.
|
||||
if api.b.ExtRPCEnabled() && !api.b.AccountManager().Config().InsecureUnlockAllowed {
|
||||
return false, errors.New("account unlock with HTTP access is forbidden")
|
||||
}
|
||||
|
||||
const max = uint64(time.Duration(gomath.MaxInt64) / time.Second)
|
||||
var d time.Duration
|
||||
if duration == nil {
|
||||
d = 300 * time.Second
|
||||
} else if *duration > max {
|
||||
return false, errors.New("unlock duration too large")
|
||||
} else {
|
||||
d = time.Duration(*duration) * time.Second
|
||||
}
|
||||
ks, err := fetchKeystore(api.am)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = ks.TimedUnlock(accounts.Account{Address: addr}, password, d)
|
||||
if err != nil {
|
||||
log.Warn("Failed account unlock attempt", "address", addr, "err", err)
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// LockAccount will lock the account associated with the given address when it's unlocked.
|
||||
func (api *PersonalAccountAPI) LockAccount(addr common.Address) bool {
|
||||
if ks, err := fetchKeystore(api.am); err == nil {
|
||||
return ks.Lock(addr) == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// signTransaction sets defaults and signs the given transaction
|
||||
// NOTE: the caller needs to ensure that the nonceLock is held, if applicable,
|
||||
// and release it after the transaction has been submitted to the tx pool
|
||||
func (api *PersonalAccountAPI) signTransaction(ctx context.Context, args *TransactionArgs, passwd string) (*types.Transaction, error) {
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: args.from()}
|
||||
wallet, err := api.am.Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set some sanity defaults and terminate on failure
|
||||
if err := args.setDefaults(ctx, api.b, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assemble the transaction and sign with the wallet
|
||||
tx := args.ToTransaction(types.LegacyTxType)
|
||||
|
||||
return wallet.SignTxWithPassphrase(account, passwd, tx, api.b.ChainConfig().ChainID)
|
||||
}
|
||||
|
||||
// SendTransaction will create a transaction from the given arguments and
|
||||
// tries to sign it with the key associated with args.From. If the given
|
||||
// passwd isn't able to decrypt the key it fails.
|
||||
func (api *PersonalAccountAPI) SendTransaction(ctx context.Context, args TransactionArgs, passwd string) (common.Hash, error) {
|
||||
if args.Nonce == nil {
|
||||
// Hold the mutex around signing to prevent concurrent assignment of
|
||||
// the same nonce to multiple accounts.
|
||||
api.nonceLock.LockAddr(args.from())
|
||||
defer api.nonceLock.UnlockAddr(args.from())
|
||||
}
|
||||
if args.IsEIP4844() {
|
||||
return common.Hash{}, errBlobTxNotSupported
|
||||
}
|
||||
signed, err := api.signTransaction(ctx, &args, passwd)
|
||||
if err != nil {
|
||||
log.Warn("Failed transaction send attempt", "from", args.from(), "to", args.To, "value", args.Value.ToInt(), "err", err)
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return SubmitTransaction(ctx, api.b, signed)
|
||||
}
|
||||
|
||||
// SignTransaction will create a transaction from the given arguments and
|
||||
// tries to sign it with the key associated with args.From. If the given passwd isn't
|
||||
// able to decrypt the key it fails. The transaction is returned in RLP-form, not broadcast
|
||||
// to other nodes
|
||||
func (api *PersonalAccountAPI) SignTransaction(ctx context.Context, args TransactionArgs, passwd string) (*SignTransactionResult, error) {
|
||||
// No need to obtain the noncelock mutex, since we won't be sending this
|
||||
// tx into the transaction pool, but right back to the user
|
||||
if args.From == nil {
|
||||
return nil, errors.New("sender not specified")
|
||||
}
|
||||
if args.Gas == nil {
|
||||
return nil, errors.New("gas not specified")
|
||||
}
|
||||
if args.GasPrice == nil && (args.MaxFeePerGas == nil || args.MaxPriorityFeePerGas == nil) {
|
||||
return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas")
|
||||
}
|
||||
if args.IsEIP4844() {
|
||||
return nil, errBlobTxNotSupported
|
||||
}
|
||||
if args.Nonce == nil {
|
||||
return nil, errors.New("nonce not specified")
|
||||
}
|
||||
// Before actually signing the transaction, ensure the transaction fee is reasonable.
|
||||
tx := args.ToTransaction(types.LegacyTxType)
|
||||
if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signed, err := api.signTransaction(ctx, &args, passwd)
|
||||
if err != nil {
|
||||
log.Warn("Failed transaction sign attempt", "from", args.from(), "to", args.To, "value", args.Value.ToInt(), "err", err)
|
||||
return nil, err
|
||||
}
|
||||
data, err := signed.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SignTransactionResult{data, signed}, nil
|
||||
}
|
||||
|
||||
// Sign calculates an Ethereum ECDSA signature for:
|
||||
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))
|
||||
//
|
||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||
// where the V value will be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// The key used to calculate the signature is decrypted with the given password.
|
||||
//
|
||||
// https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal#personal-sign
|
||||
func (api *PersonalAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) {
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr}
|
||||
|
||||
wallet, err := api.b.AccountManager().Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assemble sign the data with the wallet
|
||||
signature, err := wallet.SignTextWithPassphrase(account, passwd, data)
|
||||
if err != nil {
|
||||
log.Warn("Failed data sign attempt", "address", addr, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// EcRecover returns the address for the account that was used to create the signature.
|
||||
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||
// the address of:
|
||||
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
|
||||
// addr = ecrecover(hash, signature)
|
||||
//
|
||||
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
|
||||
// the V value must be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal#personal-ecrecover
|
||||
func (api *PersonalAccountAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
|
||||
if len(sig) != crypto.SignatureLength {
|
||||
return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength)
|
||||
}
|
||||
if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 {
|
||||
return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)")
|
||||
}
|
||||
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
||||
|
||||
rpk, err := crypto.SigToPub(accounts.TextHash(data), sig)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
return crypto.PubkeyToAddress(*rpk), nil
|
||||
}
|
||||
|
||||
// InitializeWallet initializes a new wallet at the provided URL, by generating and returning a new private key.
|
||||
func (api *PersonalAccountAPI) InitializeWallet(ctx context.Context, url string) (string, error) {
|
||||
wallet, err := api.am.Wallet(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
entropy, err := bip39.NewEntropy(256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mnemonic, err := bip39.NewMnemonic(entropy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
seed := bip39.NewSeed(mnemonic, "")
|
||||
|
||||
switch wallet := wallet.(type) {
|
||||
case *scwallet.Wallet:
|
||||
return mnemonic, wallet.Initialize(seed)
|
||||
default:
|
||||
return "", errors.New("specified wallet does not support initialization")
|
||||
}
|
||||
}
|
||||
|
||||
// Unpair deletes a pairing between wallet and geth.
|
||||
func (api *PersonalAccountAPI) Unpair(ctx context.Context, url string, pin string) error {
|
||||
wallet, err := api.am.Wallet(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch wallet := wallet.(type) {
|
||||
case *scwallet.Wallet:
|
||||
return wallet.Unpair([]byte(pin))
|
||||
default:
|
||||
return errors.New("specified wallet does not support pairing")
|
||||
}
|
||||
}
|
||||
|
||||
// BlockChainAPI provides an API to access Ethereum blockchain data.
|
||||
type BlockChainAPI struct {
|
||||
b Backend
|
||||
|
@ -118,9 +118,6 @@ func GetAPIs(apiBackend Backend) []rpc.API {
|
||||
}, {
|
||||
Namespace: "eth",
|
||||
Service: NewEthereumAccountAPI(apiBackend.AccountManager()),
|
||||
}, {
|
||||
Namespace: "personal",
|
||||
Service: NewPersonalAccountAPI(apiBackend, nonceLock),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -18,16 +18,15 @@
|
||||
package web3ext
|
||||
|
||||
var Modules = map[string]string{
|
||||
"admin": AdminJs,
|
||||
"clique": CliqueJs,
|
||||
"debug": DebugJs,
|
||||
"eth": EthJs,
|
||||
"miner": MinerJs,
|
||||
"net": NetJs,
|
||||
"personal": PersonalJs,
|
||||
"rpc": RpcJs,
|
||||
"txpool": TxpoolJs,
|
||||
"dev": DevJs,
|
||||
"admin": AdminJs,
|
||||
"clique": CliqueJs,
|
||||
"debug": DebugJs,
|
||||
"eth": EthJs,
|
||||
"miner": MinerJs,
|
||||
"net": NetJs,
|
||||
"rpc": RpcJs,
|
||||
"txpool": TxpoolJs,
|
||||
"dev": DevJs,
|
||||
}
|
||||
|
||||
const CliqueJs = `
|
||||
@ -658,62 +657,6 @@ web3._extend({
|
||||
});
|
||||
`
|
||||
|
||||
const PersonalJs = `
|
||||
web3._extend({
|
||||
property: 'personal',
|
||||
methods: [
|
||||
new web3._extend.Method({
|
||||
name: 'importRawKey',
|
||||
call: 'personal_importRawKey',
|
||||
params: 2
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'sign',
|
||||
call: 'personal_sign',
|
||||
params: 3,
|
||||
inputFormatter: [null, web3._extend.formatters.inputAddressFormatter, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'ecRecover',
|
||||
call: 'personal_ecRecover',
|
||||
params: 2
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'openWallet',
|
||||
call: 'personal_openWallet',
|
||||
params: 2
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'deriveAccount',
|
||||
call: 'personal_deriveAccount',
|
||||
params: 3
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'signTransaction',
|
||||
call: 'personal_signTransaction',
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputTransactionFormatter, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'unpair',
|
||||
call: 'personal_unpair',
|
||||
params: 2
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'initializeWallet',
|
||||
call: 'personal_initializeWallet',
|
||||
params: 1
|
||||
})
|
||||
],
|
||||
properties: [
|
||||
new web3._extend.Property({
|
||||
name: 'listWallets',
|
||||
getter: 'personal_listWallets'
|
||||
}),
|
||||
]
|
||||
})
|
||||
`
|
||||
|
||||
const RpcJs = `
|
||||
web3._extend({
|
||||
property: 'rpc',
|
||||
|
16
node/node.go
16
node/node.go
@ -375,25 +375,13 @@ func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
|
||||
// startup. It's not meant to be called at any time afterwards as it makes certain
|
||||
// assumptions about the state of the node.
|
||||
func (n *Node) startRPC() error {
|
||||
// Filter out personal api
|
||||
var apis []rpc.API
|
||||
for _, api := range n.rpcAPIs {
|
||||
if api.Namespace == "personal" {
|
||||
if n.config.EnablePersonal {
|
||||
log.Warn("Deprecated personal namespace activated")
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
apis = append(apis, api)
|
||||
}
|
||||
if err := n.startInProc(apis); err != nil {
|
||||
if err := n.startInProc(n.rpcAPIs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Configure IPC.
|
||||
if n.ipc.endpoint != "" {
|
||||
if err := n.ipc.start(apis); err != nil {
|
||||
if err := n.ipc.start(n.rpcAPIs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +294,7 @@ func typedDataRequest(data any) (*SignDataRequest, error) {
|
||||
func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
|
||||
// Returns the address for the Account that was used to create the signature.
|
||||
//
|
||||
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||
// Note, this function is compatible with eth_sign. As such it recovers
|
||||
// the address of:
|
||||
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
|
||||
// addr = ecrecover(hash, signature)
|
||||
|
Loading…
Reference in New Issue
Block a user