mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Delete signer directory
This commit is contained in:
parent
6a494c097a
commit
4dc00714bb
47 changed files with 0 additions and 276139 deletions
|
|
@ -1,670 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"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/accounts/usbwallet"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
|
|
||||||
numberOfAccountsToDerive = 10
|
|
||||||
// ExternalAPIVersion -- see extapi_changelog.md
|
|
||||||
ExternalAPIVersion = "6.1.0"
|
|
||||||
// InternalAPIVersion -- see intapi_changelog.md
|
|
||||||
InternalAPIVersion = "7.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExternalAPI defines the external API through which signing requests are made.
|
|
||||||
type ExternalAPI interface {
|
|
||||||
// List available accounts
|
|
||||||
List(ctx context.Context) ([]common.Address, error)
|
|
||||||
// New request to create a new account
|
|
||||||
New(ctx context.Context) (common.Address, error)
|
|
||||||
// SignTransaction request to sign the specified transaction
|
|
||||||
SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error)
|
|
||||||
// SignData - request to sign the given data (plus prefix)
|
|
||||||
SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error)
|
|
||||||
// SignTypedData - request to sign the given structured data (plus prefix)
|
|
||||||
SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error)
|
|
||||||
// EcRecover - recover public key from given message and signature
|
|
||||||
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
|
|
||||||
// Version info about the APIs
|
|
||||||
Version(ctx context.Context) (string, error)
|
|
||||||
// SignGnosisSafeTx signs/confirms a gnosis-safe multisig transaction
|
|
||||||
SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIClientAPI specifies what method a UI needs to implement to be able to be used as a
|
|
||||||
// UI for the signer
|
|
||||||
type UIClientAPI interface {
|
|
||||||
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
|
||||||
ApproveTx(request *SignTxRequest) (SignTxResponse, error)
|
|
||||||
// ApproveSignData prompt the user for confirmation to request to sign data
|
|
||||||
ApproveSignData(request *SignDataRequest) (SignDataResponse, error)
|
|
||||||
// ApproveListing prompt the user for confirmation to list accounts
|
|
||||||
// the list of accounts to list can be modified by the UI
|
|
||||||
ApproveListing(request *ListRequest) (ListResponse, error)
|
|
||||||
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
|
|
||||||
ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error)
|
|
||||||
// ShowError displays error message to user
|
|
||||||
ShowError(message string)
|
|
||||||
// ShowInfo displays info message to user
|
|
||||||
ShowInfo(message string)
|
|
||||||
// OnApprovedTx notifies the UI about a transaction having been successfully signed.
|
|
||||||
// This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient.
|
|
||||||
OnApprovedTx(tx ethapi.SignTransactionResult)
|
|
||||||
// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
|
|
||||||
// information
|
|
||||||
OnSignerStartup(info StartupInfo)
|
|
||||||
// OnInputRequired is invoked when clef requires user input, for example master password or
|
|
||||||
// pin-code for unlocking hardware wallets
|
|
||||||
OnInputRequired(info UserInputRequest) (UserInputResponse, error)
|
|
||||||
// RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication
|
|
||||||
RegisterUIServer(api *UIServerAPI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator defines the methods required to validate a transaction against some
|
|
||||||
// sanity defaults as well as any underlying 4byte method database.
|
|
||||||
//
|
|
||||||
// Use fourbyte.Database as an implementation. It is separated out of this package
|
|
||||||
// to allow pieces of the signer package to be used without having to load the
|
|
||||||
// 7MB embedded 4byte dump.
|
|
||||||
type Validator interface {
|
|
||||||
// ValidateTransaction does a number of checks on the supplied transaction, and
|
|
||||||
// returns either a list of warnings, or an error (indicating that the transaction
|
|
||||||
// should be immediately rejected).
|
|
||||||
ValidateTransaction(selector *string, tx *apitypes.SendTxArgs) (*apitypes.ValidationMessages, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignerAPI defines the actual implementation of ExternalAPI
|
|
||||||
type SignerAPI struct {
|
|
||||||
chainID *big.Int
|
|
||||||
am *accounts.Manager
|
|
||||||
UI UIClientAPI
|
|
||||||
validator Validator
|
|
||||||
rejectMode bool
|
|
||||||
credentials storage.Storage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata about a request
|
|
||||||
type Metadata struct {
|
|
||||||
Remote string `json:"remote"`
|
|
||||||
Local string `json:"local"`
|
|
||||||
Scheme string `json:"scheme"`
|
|
||||||
UserAgent string `json:"User-Agent"`
|
|
||||||
Origin string `json:"Origin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath string) *accounts.Manager {
|
|
||||||
var (
|
|
||||||
backends []accounts.Backend
|
|
||||||
n, p = keystore.StandardScryptN, keystore.StandardScryptP
|
|
||||||
)
|
|
||||||
if lightKDF {
|
|
||||||
n, p = keystore.LightScryptN, keystore.LightScryptP
|
|
||||||
}
|
|
||||||
// support password based accounts
|
|
||||||
if len(ksLocation) > 0 {
|
|
||||||
backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
|
|
||||||
}
|
|
||||||
if !nousb {
|
|
||||||
// Start a USB hub for Ledger hardware wallets
|
|
||||||
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
|
|
||||||
log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
|
|
||||||
} else {
|
|
||||||
backends = append(backends, ledgerhub)
|
|
||||||
log.Debug("Ledger support enabled")
|
|
||||||
}
|
|
||||||
// Start a USB hub for Trezor hardware wallets (HID version)
|
|
||||||
if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil {
|
|
||||||
log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err))
|
|
||||||
} else {
|
|
||||||
backends = append(backends, trezorhub)
|
|
||||||
log.Debug("Trezor support enabled via HID")
|
|
||||||
}
|
|
||||||
// Start a USB hub for Trezor hardware wallets (WebUSB version)
|
|
||||||
if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil {
|
|
||||||
log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err))
|
|
||||||
} else {
|
|
||||||
backends = append(backends, trezorhub)
|
|
||||||
log.Debug("Trezor support enabled via WebUSB")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a smart card hub
|
|
||||||
if len(scpath) > 0 {
|
|
||||||
// Sanity check that the smartcard path is valid
|
|
||||||
fi, err := os.Stat(scpath)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Smartcard socket file missing, disabling", "err", err)
|
|
||||||
} else {
|
|
||||||
if fi.Mode()&os.ModeType != os.ModeSocket {
|
|
||||||
log.Error("Invalid smartcard socket file type", "path", scpath, "type", fi.Mode().String())
|
|
||||||
} else {
|
|
||||||
if schub, err := scwallet.NewHub(scpath, scwallet.Scheme, ksLocation); err != nil {
|
|
||||||
log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err))
|
|
||||||
} else {
|
|
||||||
backends = append(backends, schub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clef doesn't allow insecure http account unlock.
|
|
||||||
return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, backends...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetadataFromContext extracts Metadata from a given context.Context
|
|
||||||
func MetadataFromContext(ctx context.Context) Metadata {
|
|
||||||
info := rpc.PeerInfoFromContext(ctx)
|
|
||||||
|
|
||||||
m := Metadata{"NA", "NA", "NA", "", ""} // batman
|
|
||||||
|
|
||||||
if info.Transport != "" {
|
|
||||||
if info.Transport == "http" {
|
|
||||||
m.Scheme = info.HTTP.Version
|
|
||||||
}
|
|
||||||
m.Scheme = info.Transport
|
|
||||||
}
|
|
||||||
if info.RemoteAddr != "" {
|
|
||||||
m.Remote = info.RemoteAddr
|
|
||||||
}
|
|
||||||
if info.HTTP.Host != "" {
|
|
||||||
m.Local = info.HTTP.Host
|
|
||||||
}
|
|
||||||
m.Origin = info.HTTP.Origin
|
|
||||||
m.UserAgent = info.HTTP.UserAgent
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements Stringer interface
|
|
||||||
func (m Metadata) String() string {
|
|
||||||
s, err := json.Marshal(m)
|
|
||||||
if err == nil {
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// types for the requests/response types between signer and UI
|
|
||||||
type (
|
|
||||||
// SignTxRequest contains info about a Transaction to sign
|
|
||||||
SignTxRequest struct {
|
|
||||||
Transaction apitypes.SendTxArgs `json:"transaction"`
|
|
||||||
Callinfo []apitypes.ValidationInfo `json:"call_info"`
|
|
||||||
Meta Metadata `json:"meta"`
|
|
||||||
}
|
|
||||||
// SignTxResponse result from SignTxRequest
|
|
||||||
SignTxResponse struct {
|
|
||||||
//The UI may make changes to the TX
|
|
||||||
Transaction apitypes.SendTxArgs `json:"transaction"`
|
|
||||||
Approved bool `json:"approved"`
|
|
||||||
}
|
|
||||||
SignDataRequest struct {
|
|
||||||
ContentType string `json:"content_type"`
|
|
||||||
Address common.MixedcaseAddress `json:"address"`
|
|
||||||
Rawdata []byte `json:"raw_data"`
|
|
||||||
Messages []*apitypes.NameValueType `json:"messages"`
|
|
||||||
Callinfo []apitypes.ValidationInfo `json:"call_info"`
|
|
||||||
Hash hexutil.Bytes `json:"hash"`
|
|
||||||
Meta Metadata `json:"meta"`
|
|
||||||
}
|
|
||||||
SignDataResponse struct {
|
|
||||||
Approved bool `json:"approved"`
|
|
||||||
}
|
|
||||||
NewAccountRequest struct {
|
|
||||||
Meta Metadata `json:"meta"`
|
|
||||||
}
|
|
||||||
NewAccountResponse struct {
|
|
||||||
Approved bool `json:"approved"`
|
|
||||||
}
|
|
||||||
ListRequest struct {
|
|
||||||
Accounts []accounts.Account `json:"accounts"`
|
|
||||||
Meta Metadata `json:"meta"`
|
|
||||||
}
|
|
||||||
ListResponse struct {
|
|
||||||
Accounts []accounts.Account `json:"accounts"`
|
|
||||||
}
|
|
||||||
Message struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
StartupInfo struct {
|
|
||||||
Info map[string]interface{} `json:"info"`
|
|
||||||
}
|
|
||||||
UserInputRequest struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
IsPassword bool `json:"isPassword"`
|
|
||||||
}
|
|
||||||
UserInputResponse struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrRequestDenied = errors.New("request denied")
|
|
||||||
|
|
||||||
// NewSignerAPI creates a new API that can be used for Account management.
|
|
||||||
// ksLocation specifies the directory where to store the password protected private
|
|
||||||
// key that is generated when a new Account is created.
|
|
||||||
// noUSB disables USB support that is required to support hardware devices such as
|
|
||||||
// ledger and trezor.
|
|
||||||
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI {
|
|
||||||
if advancedMode {
|
|
||||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
|
||||||
}
|
|
||||||
signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials}
|
|
||||||
if !noUSB {
|
|
||||||
signer.startUSBListener()
|
|
||||||
}
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
func (api *SignerAPI) openTrezor(url accounts.URL) {
|
|
||||||
resp, err := api.UI.OnInputRequired(UserInputRequest{
|
|
||||||
Prompt: "Pin required to open Trezor wallet\n" +
|
|
||||||
"Look at the device for number positions\n\n" +
|
|
||||||
"7 | 8 | 9\n" +
|
|
||||||
"--+---+--\n" +
|
|
||||||
"4 | 5 | 6\n" +
|
|
||||||
"--+---+--\n" +
|
|
||||||
"1 | 2 | 3\n\n",
|
|
||||||
IsPassword: true,
|
|
||||||
Title: "Trezor unlock",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("failed getting trezor pin", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// We're using the URL instead of the pointer to the
|
|
||||||
// Wallet -- perhaps it is not actually present anymore
|
|
||||||
w, err := api.am.Wallet(url.String())
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("wallet unavailable", "url", url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = w.Open(resp.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("failed to open wallet", "wallet", url, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startUSBListener starts a listener for USB events, for hardware wallet interaction
|
|
||||||
func (api *SignerAPI) startUSBListener() {
|
|
||||||
eventCh := make(chan accounts.WalletEvent, 16)
|
|
||||||
am := api.am
|
|
||||||
am.Subscribe(eventCh)
|
|
||||||
// Open any wallets already attached
|
|
||||||
for _, wallet := range am.Wallets() {
|
|
||||||
if err := wallet.Open(""); err != nil {
|
|
||||||
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
|
|
||||||
if err == usbwallet.ErrTrezorPINNeeded {
|
|
||||||
go api.openTrezor(wallet.URL())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go api.derivationLoop(eventCh)
|
|
||||||
}
|
|
||||||
|
|
||||||
// derivationLoop listens for wallet events
|
|
||||||
func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) {
|
|
||||||
// Listen for wallet event till termination
|
|
||||||
for event := range events {
|
|
||||||
switch event.Kind {
|
|
||||||
case accounts.WalletArrived:
|
|
||||||
if err := event.Wallet.Open(""); err != nil {
|
|
||||||
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
|
|
||||||
if err == usbwallet.ErrTrezorPINNeeded {
|
|
||||||
go api.openTrezor(event.Wallet.URL())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case accounts.WalletOpened:
|
|
||||||
status, _ := event.Wallet.Status()
|
|
||||||
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
|
|
||||||
var derive = func(limit int, next func() accounts.DerivationPath) {
|
|
||||||
// Derive first N accounts, hardcoded for now
|
|
||||||
for i := 0; i < limit; i++ {
|
|
||||||
path := next()
|
|
||||||
if acc, err := event.Wallet.Derive(path, true); err != nil {
|
|
||||||
log.Warn("Account derivation failed", "error", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Derived account", "address", acc.Address, "path", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info("Deriving default paths")
|
|
||||||
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath))
|
|
||||||
if event.Wallet.URL().Scheme == "ledger" {
|
|
||||||
log.Info("Deriving ledger legacy paths")
|
|
||||||
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.LegacyLedgerBaseDerivationPath))
|
|
||||||
log.Info("Deriving ledger live paths")
|
|
||||||
// For ledger live, since it's based off the same (DefaultBaseDerivationPath)
|
|
||||||
// as one we've already used, we need to step it forward one step to avoid
|
|
||||||
// hitting the same path again
|
|
||||||
nextFn := accounts.LedgerLiveIterator(accounts.DefaultBaseDerivationPath)
|
|
||||||
nextFn()
|
|
||||||
derive(numberOfAccountsToDerive, nextFn)
|
|
||||||
}
|
|
||||||
case accounts.WalletDropped:
|
|
||||||
log.Info("Old wallet dropped", "url", event.Wallet.URL())
|
|
||||||
event.Wallet.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns the set of wallet this signer manages. Each wallet can contain
|
|
||||||
// multiple accounts.
|
|
||||||
func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) {
|
|
||||||
var accs = make([]accounts.Account, 0)
|
|
||||||
// accs is initialized as empty list, not nil. We use 'nil' to signal
|
|
||||||
// rejection, as opposed to an empty list.
|
|
||||||
for _, wallet := range api.am.Wallets() {
|
|
||||||
accs = append(accs, wallet.Accounts()...)
|
|
||||||
}
|
|
||||||
result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if result.Accounts == nil {
|
|
||||||
return nil, ErrRequestDenied
|
|
||||||
}
|
|
||||||
addresses := make([]common.Address, 0)
|
|
||||||
for _, acc := range result.Accounts {
|
|
||||||
addresses = append(addresses, acc.Address)
|
|
||||||
}
|
|
||||||
return addresses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new password protected Account. The private key is protected with
|
|
||||||
// the given password. Users are responsible to backup the private key that is stored
|
|
||||||
// in the keystore location that was specified when this API was created.
|
|
||||||
func (api *SignerAPI) New(ctx context.Context) (common.Address, error) {
|
|
||||||
if be := api.am.Backends(keystore.KeyStoreType); len(be) == 0 {
|
|
||||||
return common.Address{}, errors.New("password based accounts not supported")
|
|
||||||
}
|
|
||||||
if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil {
|
|
||||||
return common.Address{}, err
|
|
||||||
} else if !resp.Approved {
|
|
||||||
return common.Address{}, ErrRequestDenied
|
|
||||||
}
|
|
||||||
return api.newAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAccount is the internal method to create a new account. It should be used
|
|
||||||
// _after_ user-approval has been obtained
|
|
||||||
func (api *SignerAPI) newAccount() (common.Address, error) {
|
|
||||||
be := api.am.Backends(keystore.KeyStoreType)
|
|
||||||
if len(be) == 0 {
|
|
||||||
return common.Address{}, errors.New("password based accounts not supported")
|
|
||||||
}
|
|
||||||
// Three retries to get a valid password
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
resp, err := api.UI.OnInputRequired(UserInputRequest{
|
|
||||||
"New account password",
|
|
||||||
fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i),
|
|
||||||
true})
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("error obtaining password", "attempt", i, "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil {
|
|
||||||
api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", i+1, pwErr))
|
|
||||||
} else {
|
|
||||||
// No error
|
|
||||||
acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text)
|
|
||||||
log.Info("Your new key was generated", "address", acc.Address)
|
|
||||||
log.Warn("Please backup your key file!", "path", acc.URL.Path)
|
|
||||||
log.Warn("Please remember your password!")
|
|
||||||
return acc.Address, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise fail, with generic error message
|
|
||||||
return common.Address{}, errors.New("account creation failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
|
|
||||||
// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow
|
|
||||||
// UI-modifications to requests
|
|
||||||
func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
|
|
||||||
var intPtrModified = func(a, b *hexutil.Big) bool {
|
|
||||||
aBig := (*big.Int)(a)
|
|
||||||
bBig := (*big.Int)(b)
|
|
||||||
if aBig != nil && bBig != nil {
|
|
||||||
return aBig.Cmp(bBig) != 0
|
|
||||||
}
|
|
||||||
// One or both of them are nil
|
|
||||||
return a != b
|
|
||||||
}
|
|
||||||
|
|
||||||
modified := false
|
|
||||||
if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) {
|
|
||||||
log.Info("Sender-account changed by UI", "was", f0, "is", f1)
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) {
|
|
||||||
log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 {
|
|
||||||
modified = true
|
|
||||||
log.Info("Gas changed by UI", "was", g0, "is", g1)
|
|
||||||
}
|
|
||||||
if a, b := original.Transaction.GasPrice, new.Transaction.GasPrice; intPtrModified(a, b) {
|
|
||||||
log.Info("GasPrice changed by UI", "was", a, "is", b)
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
if a, b := original.Transaction.MaxPriorityFeePerGas, new.Transaction.MaxPriorityFeePerGas; intPtrModified(a, b) {
|
|
||||||
log.Info("maxPriorityFeePerGas changed by UI", "was", a, "is", b)
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
if a, b := original.Transaction.MaxFeePerGas, new.Transaction.MaxFeePerGas; intPtrModified(a, b) {
|
|
||||||
log.Info("maxFeePerGas changed by UI", "was", a, "is", b)
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 {
|
|
||||||
modified = true
|
|
||||||
log.Info("Value changed by UI", "was", v0, "is", v1)
|
|
||||||
}
|
|
||||||
if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 {
|
|
||||||
d0s := ""
|
|
||||||
d1s := ""
|
|
||||||
if d0 != nil {
|
|
||||||
d0s = hexutil.Encode(*d0)
|
|
||||||
}
|
|
||||||
if d1 != nil {
|
|
||||||
d1s = hexutil.Encode(*d1)
|
|
||||||
}
|
|
||||||
if d1s != d0s {
|
|
||||||
modified = true
|
|
||||||
log.Info("Data changed by UI", "was", d0s, "is", d1s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 {
|
|
||||||
modified = true
|
|
||||||
log.Info("Nonce changed by UI", "was", n0, "is", n1)
|
|
||||||
}
|
|
||||||
return modified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *SignerAPI) lookupPassword(address common.Address) (string, error) {
|
|
||||||
return api.credentials.Get(address.Hex())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) {
|
|
||||||
// Look up the password and return if available
|
|
||||||
if pw, err := api.lookupPassword(address); err == nil {
|
|
||||||
return pw, nil
|
|
||||||
}
|
|
||||||
// Password unavailable, request it from the user
|
|
||||||
pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true})
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("error obtaining password", "error", err)
|
|
||||||
// We'll not forward the error here, in case the error contains info about the response from the UI,
|
|
||||||
// which could leak the password if it was malformed json or something
|
|
||||||
return "", errors.New("internal error")
|
|
||||||
}
|
|
||||||
return pwResp.Text, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
|
|
||||||
func (api *SignerAPI) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
result SignTxResponse
|
|
||||||
)
|
|
||||||
msgs, err := api.validator.ValidateTransaction(methodSelector, &args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// If we are in 'rejectMode', then reject rather than show the user warnings
|
|
||||||
if api.rejectMode {
|
|
||||||
if err := msgs.GetWarnings(); err != nil {
|
|
||||||
log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if args.ChainID != nil {
|
|
||||||
requestedChainId := (*big.Int)(args.ChainID)
|
|
||||||
if api.chainID.Cmp(requestedChainId) != 0 {
|
|
||||||
log.Error("Signing request with wrong chain id", "requested", requestedChainId, "configured", api.chainID)
|
|
||||||
return nil, fmt.Errorf("requested chainid %d does not match the configuration of the signer",
|
|
||||||
requestedChainId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req := SignTxRequest{
|
|
||||||
Transaction: args,
|
|
||||||
Meta: MetadataFromContext(ctx),
|
|
||||||
Callinfo: msgs.Messages,
|
|
||||||
}
|
|
||||||
// Process approval
|
|
||||||
result, err = api.UI.ApproveTx(&req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !result.Approved {
|
|
||||||
return nil, ErrRequestDenied
|
|
||||||
}
|
|
||||||
// Log changes made by the UI to the signing-request
|
|
||||||
logDiff(&req, &result)
|
|
||||||
var (
|
|
||||||
acc accounts.Account
|
|
||||||
wallet accounts.Wallet
|
|
||||||
)
|
|
||||||
acc = accounts.Account{Address: result.Transaction.From.Address()}
|
|
||||||
wallet, err = api.am.Find(acc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Convert fields into a real transaction
|
|
||||||
var unsignedTx = result.Transaction.ToTransaction()
|
|
||||||
// Get the password for the transaction
|
|
||||||
pw, err := api.lookupOrQueryPassword(acc.Address, "Account password",
|
|
||||||
fmt.Sprintf("Please enter the password for account %s", acc.Address.String()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// The one to sign is the one that was returned from the UI
|
|
||||||
signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.chainID)
|
|
||||||
if err != nil {
|
|
||||||
api.UI.ShowError(err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := signedTx.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response := ethapi.SignTransactionResult{Raw: data, Tx: signedTx}
|
|
||||||
|
|
||||||
// Finally, send the signed tx to the UI
|
|
||||||
api.UI.OnApprovedTx(response)
|
|
||||||
// ...and to the external caller
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
|
|
||||||
// Do the usual validations, but on the last-stage transaction
|
|
||||||
args := gnosisTx.ArgsForValidation()
|
|
||||||
msgs, err := api.validator.ValidateTransaction(methodSelector, args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// If we are in 'rejectMode', then reject rather than show the user warnings
|
|
||||||
if api.rejectMode {
|
|
||||||
if err := msgs.GetWarnings(); err != nil {
|
|
||||||
log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
typedData := gnosisTx.ToTypedData()
|
|
||||||
// might aswell error early.
|
|
||||||
// we are expected to sign. If our calculated hash does not match what they want,
|
|
||||||
// The gnosis safetx input contains a 'safeTxHash' which is the expected safeTxHash that
|
|
||||||
sighash, _, err := apitypes.TypedDataAndHash(typedData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) {
|
|
||||||
// It might be the case that the json is missing chain id.
|
|
||||||
if gnosisTx.ChainId == nil {
|
|
||||||
gnosisTx.ChainId = (*math.HexOrDecimal256)(api.chainID)
|
|
||||||
typedData = gnosisTx.ToTypedData()
|
|
||||||
sighash, _, _ = apitypes.TypedDataAndHash(typedData)
|
|
||||||
if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) {
|
|
||||||
return nil, fmt.Errorf("mismatched safeTxHash; have %#x want %#x", sighash, gnosisTx.InputExpHash[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex())
|
|
||||||
|
|
||||||
gnosisTx.Signature = signature
|
|
||||||
gnosisTx.SafeTxHash = common.BytesToHash(preimage)
|
|
||||||
gnosisTx.Sender = *checkSummedSender // Must be checksummed to be accepted by relay
|
|
||||||
|
|
||||||
return &gnosisTx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the external api version. This method does not require user acceptance. Available methods are
|
|
||||||
// available via enumeration anyway, and this info does not contain user-specific data
|
|
||||||
func (api *SignerAPI) Version(ctx context.Context) (string, error) {
|
|
||||||
return ExternalAPIVersion, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
// Copyright 2018 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 core_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/fourbyte"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Used for testing
|
|
||||||
type headlessUi struct {
|
|
||||||
approveCh chan string // to send approve/deny
|
|
||||||
inputCh chan string // to send password
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
|
||||||
input := <-ui.inputCh
|
|
||||||
return core.UserInputResponse{Text: input}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) OnSignerStartup(info core.StartupInfo) {}
|
|
||||||
func (ui *headlessUi) RegisterUIServer(api *core.UIServerAPI) {}
|
|
||||||
func (ui *headlessUi) OnApprovedTx(tx ethapi.SignTransactionResult) {}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
|
||||||
switch <-ui.approveCh {
|
|
||||||
case "Y":
|
|
||||||
return core.SignTxResponse{request.Transaction, true}, nil
|
|
||||||
case "M": // modify
|
|
||||||
// The headless UI always modifies the transaction
|
|
||||||
old := big.Int(request.Transaction.Value)
|
|
||||||
newVal := new(big.Int).Add(&old, big.NewInt(1))
|
|
||||||
request.Transaction.Value = hexutil.Big(*newVal)
|
|
||||||
return core.SignTxResponse{request.Transaction, true}, nil
|
|
||||||
default:
|
|
||||||
return core.SignTxResponse{request.Transaction, false}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
|
||||||
approved := (<-ui.approveCh == "Y")
|
|
||||||
return core.SignDataResponse{approved}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
|
||||||
approval := <-ui.approveCh
|
|
||||||
//fmt.Printf("approval %s\n", approval)
|
|
||||||
switch approval {
|
|
||||||
case "A":
|
|
||||||
return core.ListResponse{request.Accounts}, nil
|
|
||||||
case "1":
|
|
||||||
l := make([]accounts.Account, 1)
|
|
||||||
l[0] = request.Accounts[1]
|
|
||||||
return core.ListResponse{l}, nil
|
|
||||||
default:
|
|
||||||
return core.ListResponse{nil}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
|
||||||
if <-ui.approveCh == "Y" {
|
|
||||||
return core.NewAccountResponse{true}, nil
|
|
||||||
}
|
|
||||||
return core.NewAccountResponse{false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ShowError(message string) {
|
|
||||||
//stdout is used by communication
|
|
||||||
fmt.Fprintln(os.Stderr, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *headlessUi) ShowInfo(message string) {
|
|
||||||
//stdout is used by communication
|
|
||||||
fmt.Fprintln(os.Stderr, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tmpDirName(t *testing.T) string {
|
|
||||||
d := t.TempDir()
|
|
||||||
d, err := filepath.EvalSymlinks(d)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup(t *testing.T) (*core.SignerAPI, *headlessUi) {
|
|
||||||
db, err := fourbyte.New()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
|
||||||
am := core.StartClefAccountManager(tmpDirName(t), true, true, "")
|
|
||||||
api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{})
|
|
||||||
return api, ui
|
|
||||||
}
|
|
||||||
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
|
||||||
ui.approveCh <- "Y"
|
|
||||||
ui.inputCh <- "a_long_password"
|
|
||||||
_, err := api.New(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Some time to allow changes to propagate
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
func failCreateAccountWithPassword(ui *headlessUi, api *core.SignerAPI, password string, t *testing.T) {
|
|
||||||
ui.approveCh <- "Y"
|
|
||||||
// We will be asked three times to provide a suitable password
|
|
||||||
ui.inputCh <- password
|
|
||||||
ui.inputCh <- password
|
|
||||||
ui.inputCh <- password
|
|
||||||
|
|
||||||
addr, err := api.New(context.Background())
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Should have returned an error")
|
|
||||||
}
|
|
||||||
if addr != (common.Address{}) {
|
|
||||||
t.Fatal("Empty address should be returned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func failCreateAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
|
||||||
ui.approveCh <- "N"
|
|
||||||
addr, err := api.New(context.Background())
|
|
||||||
if err != core.ErrRequestDenied {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if addr != (common.Address{}) {
|
|
||||||
t.Fatal("Empty address should be returned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func list(ui *headlessUi, api *core.SignerAPI, t *testing.T) ([]common.Address, error) {
|
|
||||||
ui.approveCh <- "A"
|
|
||||||
return api.List(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAcc(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
api, control := setup(t)
|
|
||||||
verifyNum := func(num int) {
|
|
||||||
list, err := list(control, api, t)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if len(list) != num {
|
|
||||||
t.Errorf("Expected %d accounts, got %d", num, len(list))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Testing create and create-deny
|
|
||||||
createAccount(control, api, t)
|
|
||||||
createAccount(control, api, t)
|
|
||||||
failCreateAccount(control, api, t)
|
|
||||||
failCreateAccount(control, api, t)
|
|
||||||
createAccount(control, api, t)
|
|
||||||
failCreateAccount(control, api, t)
|
|
||||||
createAccount(control, api, t)
|
|
||||||
failCreateAccount(control, api, t)
|
|
||||||
verifyNum(4)
|
|
||||||
|
|
||||||
// Fail to create this, due to bad password
|
|
||||||
failCreateAccountWithPassword(control, api, "short", t)
|
|
||||||
failCreateAccountWithPassword(control, api, "longerbutbad\rfoo", t)
|
|
||||||
verifyNum(4)
|
|
||||||
|
|
||||||
// Testing listing:
|
|
||||||
// Listing one Account
|
|
||||||
control.approveCh <- "1"
|
|
||||||
list, err := api.List(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(list) != 1 {
|
|
||||||
t.Fatalf("List should only show one Account")
|
|
||||||
}
|
|
||||||
// Listing denied
|
|
||||||
control.approveCh <- "Nope"
|
|
||||||
list, err = api.List(context.Background())
|
|
||||||
if len(list) != 0 {
|
|
||||||
t.Fatalf("List should be empty")
|
|
||||||
}
|
|
||||||
if err != core.ErrRequestDenied {
|
|
||||||
t.Fatal("Expected deny")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mkTestTx(from common.MixedcaseAddress) apitypes.SendTxArgs {
|
|
||||||
to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
|
|
||||||
gas := hexutil.Uint64(21000)
|
|
||||||
gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
|
|
||||||
value := (hexutil.Big)(*big.NewInt(1e18))
|
|
||||||
nonce := (hexutil.Uint64)(0)
|
|
||||||
data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
|
|
||||||
tx := apitypes.SendTxArgs{
|
|
||||||
From: from,
|
|
||||||
To: &to,
|
|
||||||
Gas: gas,
|
|
||||||
GasPrice: &gasPrice,
|
|
||||||
Value: value,
|
|
||||||
Data: &data,
|
|
||||||
Nonce: nonce}
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignTx(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
var (
|
|
||||||
list []common.Address
|
|
||||||
res, res2 *ethapi.SignTransactionResult
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
api, control := setup(t)
|
|
||||||
createAccount(control, api, t)
|
|
||||||
control.approveCh <- "A"
|
|
||||||
list, err = api.List(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(list) == 0 {
|
|
||||||
t.Fatal("Unexpected empty list")
|
|
||||||
}
|
|
||||||
a := common.NewMixedcaseAddress(list[0])
|
|
||||||
|
|
||||||
methodSig := "test(uint)"
|
|
||||||
tx := mkTestTx(a)
|
|
||||||
|
|
||||||
control.approveCh <- "Y"
|
|
||||||
control.inputCh <- "wrongpassword"
|
|
||||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
|
|
||||||
if res != nil {
|
|
||||||
t.Errorf("Expected nil-response, got %v", res)
|
|
||||||
}
|
|
||||||
if err != keystore.ErrDecrypt {
|
|
||||||
t.Errorf("Expected ErrLocked! %v", err)
|
|
||||||
}
|
|
||||||
control.approveCh <- "No way"
|
|
||||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
|
|
||||||
if res != nil {
|
|
||||||
t.Errorf("Expected nil-response, got %v", res)
|
|
||||||
}
|
|
||||||
if err != core.ErrRequestDenied {
|
|
||||||
t.Errorf("Expected ErrRequestDenied! %v", err)
|
|
||||||
}
|
|
||||||
// Sign with correct password
|
|
||||||
control.approveCh <- "Y"
|
|
||||||
control.inputCh <- "a_long_password"
|
|
||||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
parsedTx := &types.Transaction{}
|
|
||||||
rlp.DecodeBytes(res.Raw, parsedTx)
|
|
||||||
|
|
||||||
//The tx should NOT be modified by the UI
|
|
||||||
if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 {
|
|
||||||
t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value())
|
|
||||||
}
|
|
||||||
control.approveCh <- "Y"
|
|
||||||
control.inputCh <- "a_long_password"
|
|
||||||
|
|
||||||
res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(res.Raw, res2.Raw) {
|
|
||||||
t.Error("Expected tx to be unmodified by UI")
|
|
||||||
}
|
|
||||||
|
|
||||||
//The tx is modified by the UI
|
|
||||||
control.approveCh <- "M"
|
|
||||||
control.inputCh <- "a_long_password"
|
|
||||||
|
|
||||||
res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
parsedTx2 := &types.Transaction{}
|
|
||||||
rlp.DecodeBytes(res.Raw, parsedTx2)
|
|
||||||
|
|
||||||
//The tx should be modified by the UI
|
|
||||||
if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 {
|
|
||||||
t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value())
|
|
||||||
}
|
|
||||||
if bytes.Equal(res.Raw, res2.Raw) {
|
|
||||||
t.Error("Expected tx to be modified by UI")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
// Copyright 2019 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 apitypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBytesPadding(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
Type string
|
|
||||||
Input []byte
|
|
||||||
Output []byte // nil => error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// Fail on wrong length
|
|
||||||
Type: "bytes20",
|
|
||||||
Input: []byte{},
|
|
||||||
Output: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bytes1",
|
|
||||||
Input: []byte{1},
|
|
||||||
Output: []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bytes1",
|
|
||||||
Input: []byte{1, 2},
|
|
||||||
Output: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bytes7",
|
|
||||||
Input: []byte{1, 2, 3, 4, 5, 6, 7},
|
|
||||||
Output: []byte{1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bytes32",
|
|
||||||
Input: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32},
|
|
||||||
Output: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bytes32",
|
|
||||||
Input: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33},
|
|
||||||
Output: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
d := TypedData{}
|
|
||||||
for i, test := range tests {
|
|
||||||
val, err := d.EncodePrimitiveValue(test.Type, test.Input, 1)
|
|
||||||
if test.Output == nil {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d: expected error, got no error (result %x)", i, val)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: expected no error, got %v", i, err)
|
|
||||||
}
|
|
||||||
if len(val) != 32 {
|
|
||||||
t.Errorf("test %d: expected len 32, got %d", i, len(val))
|
|
||||||
}
|
|
||||||
if !bytes.Equal(val, test.Output) {
|
|
||||||
t.Errorf("test %d: expected %x, got %x", i, test.Output, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAddress(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
Input interface{}
|
|
||||||
Output []byte // nil => error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Input: [20]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14},
|
|
||||||
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: "0x0102030405060708090A0B0C0D0E0F1011121314",
|
|
||||||
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14},
|
|
||||||
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
|
|
||||||
},
|
|
||||||
// Various error-cases:
|
|
||||||
{Input: "0x000102030405060708090A0B0C0D0E0F1011121314"}, // too long string
|
|
||||||
{Input: "0x01"}, // too short string
|
|
||||||
{Input: ""},
|
|
||||||
{Input: [32]byte{}}, // too long fixed-size array
|
|
||||||
{Input: [21]byte{}}, // too long fixed-size array
|
|
||||||
{Input: make([]byte, 19)}, // too short slice
|
|
||||||
{Input: make([]byte, 21)}, // too long slice
|
|
||||||
{Input: nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
d := TypedData{}
|
|
||||||
for i, test := range tests {
|
|
||||||
val, err := d.EncodePrimitiveValue("address", test.Input, 1)
|
|
||||||
if test.Output == nil {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d: expected error, got no error (result %x)", i, val)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: expected no error, got %v", i, err)
|
|
||||||
}
|
|
||||||
if have, want := len(val), 32; have != want {
|
|
||||||
t.Errorf("test %d: have len %d, want %d", i, have, want)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(val, test.Output) {
|
|
||||||
t.Errorf("test %d: want %x, have %x", i, test.Output, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseBytes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for i, tt := range []struct {
|
|
||||||
v interface{}
|
|
||||||
exp []byte
|
|
||||||
}{
|
|
||||||
{"0x", []byte{}},
|
|
||||||
{"0x1234", []byte{0x12, 0x34}},
|
|
||||||
{[]byte{12, 34}, []byte{12, 34}},
|
|
||||||
{hexutil.Bytes([]byte{12, 34}), []byte{12, 34}},
|
|
||||||
{"1234", nil}, // not a proper hex-string
|
|
||||||
{"0x01233", nil}, // nibbles should be rejected
|
|
||||||
{"not a hex string", nil},
|
|
||||||
{15, nil},
|
|
||||||
{nil, nil},
|
|
||||||
{[2]byte{12, 34}, []byte{12, 34}},
|
|
||||||
{[8]byte{12, 34, 56, 78, 90, 12, 34, 56}, []byte{12, 34, 56, 78, 90, 12, 34, 56}},
|
|
||||||
{[16]byte{12, 34, 56, 78, 90, 12, 34, 56, 12, 34, 56, 78, 90, 12, 34, 56}, []byte{12, 34, 56, 78, 90, 12, 34, 56, 12, 34, 56, 78, 90, 12, 34, 56}},
|
|
||||||
} {
|
|
||||||
out, ok := parseBytes(tt.v)
|
|
||||||
if tt.exp == nil {
|
|
||||||
if ok || out != nil {
|
|
||||||
t.Errorf("test %d: expected !ok, got ok = %v with out = %x", i, ok, out)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("test %d: expected ok got !ok", i)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(out, tt.exp) {
|
|
||||||
t.Errorf("test %d: expected %x got %x", i, tt.exp, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseInteger(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for i, tt := range []struct {
|
|
||||||
t string
|
|
||||||
v interface{}
|
|
||||||
exp *big.Int
|
|
||||||
}{
|
|
||||||
{"uint32", "-123", nil},
|
|
||||||
{"int32", "-123", big.NewInt(-123)},
|
|
||||||
{"int32", big.NewInt(-124), big.NewInt(-124)},
|
|
||||||
{"uint32", "0xff", big.NewInt(0xff)},
|
|
||||||
{"int8", "0xffff", nil},
|
|
||||||
} {
|
|
||||||
res, err := parseInteger(tt.t, tt.v)
|
|
||||||
if tt.exp == nil && res == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tt.exp == nil && res != nil {
|
|
||||||
t.Errorf("test %d, got %v, expected nil", i, res)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tt.exp != nil && res == nil {
|
|
||||||
t.Errorf("test %d, got '%v', expected %v", i, err, tt.exp)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tt.exp.Cmp(res) != 0 {
|
|
||||||
t.Errorf("test %d, got %v expected %v", i, res, tt.exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertStringDataToSlice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
slice := []string{"a", "b", "c"}
|
|
||||||
var it interface{} = slice
|
|
||||||
_, err := convertDataToSlice(it)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertUint256DataToSlice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
slice := []*math.HexOrDecimal256{
|
|
||||||
math.NewHexOrDecimal256(1),
|
|
||||||
math.NewHexOrDecimal256(2),
|
|
||||||
math.NewHexOrDecimal256(3),
|
|
||||||
}
|
|
||||||
var it interface{} = slice
|
|
||||||
_, err := convertDataToSlice(it)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertAddressDataToSlice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
slice := []common.Address{
|
|
||||||
common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
|
||||||
common.HexToAddress("0x0000000000000000000000000000000000000002"),
|
|
||||||
common.HexToAddress("0x0000000000000000000000000000000000000003"),
|
|
||||||
}
|
|
||||||
var it interface{} = slice
|
|
||||||
_, err := convertDataToSlice(it)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,836 +0,0 @@
|
||||||
// Copyright 2018 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 apitypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Za-z](\w*)(\[\])?$`)
|
|
||||||
|
|
||||||
type ValidationInfo struct {
|
|
||||||
Typ string `json:"type"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
type ValidationMessages struct {
|
|
||||||
Messages []ValidationInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
WARN = "WARNING"
|
|
||||||
CRIT = "CRITICAL"
|
|
||||||
INFO = "Info"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (vs *ValidationMessages) Crit(msg string) {
|
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{CRIT, msg})
|
|
||||||
}
|
|
||||||
func (vs *ValidationMessages) Warn(msg string) {
|
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{WARN, msg})
|
|
||||||
}
|
|
||||||
func (vs *ValidationMessages) Info(msg string) {
|
|
||||||
vs.Messages = append(vs.Messages, ValidationInfo{INFO, msg})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWarnings returns an error with all messages of type WARN of above, or nil if no warnings were present
|
|
||||||
func (v *ValidationMessages) GetWarnings() error {
|
|
||||||
var messages []string
|
|
||||||
for _, msg := range v.Messages {
|
|
||||||
if msg.Typ == WARN || msg.Typ == CRIT {
|
|
||||||
messages = append(messages, msg.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(messages) > 0 {
|
|
||||||
return fmt.Errorf("validation failed: %s", strings.Join(messages, ","))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendTxArgs represents the arguments to submit a transaction
|
|
||||||
// This struct is identical to ethapi.TransactionArgs, except for the usage of
|
|
||||||
// common.MixedcaseAddress in From and To
|
|
||||||
type SendTxArgs struct {
|
|
||||||
From common.MixedcaseAddress `json:"from"`
|
|
||||||
To *common.MixedcaseAddress `json:"to"`
|
|
||||||
Gas hexutil.Uint64 `json:"gas"`
|
|
||||||
GasPrice *hexutil.Big `json:"gasPrice"`
|
|
||||||
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
|
|
||||||
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
|
|
||||||
Value hexutil.Big `json:"value"`
|
|
||||||
Nonce hexutil.Uint64 `json:"nonce"`
|
|
||||||
|
|
||||||
// We accept "data" and "input" for backwards-compatibility reasons.
|
|
||||||
// "input" is the newer name and should be preferred by clients.
|
|
||||||
// Issue detail: https://github.com/ethereum/go-ethereum/issues/15628
|
|
||||||
Data *hexutil.Bytes `json:"data"`
|
|
||||||
Input *hexutil.Bytes `json:"input,omitempty"`
|
|
||||||
|
|
||||||
// For non-legacy transactions
|
|
||||||
AccessList *types.AccessList `json:"accessList,omitempty"`
|
|
||||||
ChainID *hexutil.Big `json:"chainId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args SendTxArgs) String() string {
|
|
||||||
s, err := json.Marshal(args)
|
|
||||||
if err == nil {
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTransaction converts the arguments to a transaction.
|
|
||||||
func (args *SendTxArgs) ToTransaction() *types.Transaction {
|
|
||||||
// Add the To-field, if specified
|
|
||||||
var to *common.Address
|
|
||||||
if args.To != nil {
|
|
||||||
dstAddr := args.To.Address()
|
|
||||||
to = &dstAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
var input []byte
|
|
||||||
if args.Input != nil {
|
|
||||||
input = *args.Input
|
|
||||||
} else if args.Data != nil {
|
|
||||||
input = *args.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
var data types.TxData
|
|
||||||
switch {
|
|
||||||
case args.MaxFeePerGas != nil:
|
|
||||||
al := types.AccessList{}
|
|
||||||
if args.AccessList != nil {
|
|
||||||
al = *args.AccessList
|
|
||||||
}
|
|
||||||
data = &types.DynamicFeeTx{
|
|
||||||
To: to,
|
|
||||||
ChainID: (*big.Int)(args.ChainID),
|
|
||||||
Nonce: uint64(args.Nonce),
|
|
||||||
Gas: uint64(args.Gas),
|
|
||||||
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
|
|
||||||
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
|
|
||||||
Value: (*big.Int)(&args.Value),
|
|
||||||
Data: input,
|
|
||||||
AccessList: al,
|
|
||||||
}
|
|
||||||
case args.AccessList != nil:
|
|
||||||
data = &types.AccessListTx{
|
|
||||||
To: to,
|
|
||||||
ChainID: (*big.Int)(args.ChainID),
|
|
||||||
Nonce: uint64(args.Nonce),
|
|
||||||
Gas: uint64(args.Gas),
|
|
||||||
GasPrice: (*big.Int)(args.GasPrice),
|
|
||||||
Value: (*big.Int)(&args.Value),
|
|
||||||
Data: input,
|
|
||||||
AccessList: *args.AccessList,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
data = &types.LegacyTx{
|
|
||||||
To: to,
|
|
||||||
Nonce: uint64(args.Nonce),
|
|
||||||
Gas: uint64(args.Gas),
|
|
||||||
GasPrice: (*big.Int)(args.GasPrice),
|
|
||||||
Value: (*big.Int)(&args.Value),
|
|
||||||
Data: input,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return types.NewTx(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SigFormat struct {
|
|
||||||
Mime string
|
|
||||||
ByteVersion byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
IntendedValidator = SigFormat{
|
|
||||||
accounts.MimetypeDataWithValidator,
|
|
||||||
0x00,
|
|
||||||
}
|
|
||||||
DataTyped = SigFormat{
|
|
||||||
accounts.MimetypeTypedData,
|
|
||||||
0x01,
|
|
||||||
}
|
|
||||||
ApplicationClique = SigFormat{
|
|
||||||
accounts.MimetypeClique,
|
|
||||||
0x02,
|
|
||||||
}
|
|
||||||
TextPlain = SigFormat{
|
|
||||||
accounts.MimetypeTextPlain,
|
|
||||||
0x45,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type ValidatorData struct {
|
|
||||||
Address common.Address
|
|
||||||
Message hexutil.Bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypedData is a type to encapsulate EIP-712 typed messages
|
|
||||||
type TypedData struct {
|
|
||||||
Types Types `json:"types"`
|
|
||||||
PrimaryType string `json:"primaryType"`
|
|
||||||
Domain TypedDataDomain `json:"domain"`
|
|
||||||
Message TypedDataMessage `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type is the inner type of an EIP-712 message
|
|
||||||
type Type struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Type) isArray() bool {
|
|
||||||
return strings.HasSuffix(t.Type, "[]")
|
|
||||||
}
|
|
||||||
|
|
||||||
// typeName returns the canonical name of the type. If the type is 'Person[]', then
|
|
||||||
// this method returns 'Person'
|
|
||||||
func (t *Type) typeName() string {
|
|
||||||
if strings.HasSuffix(t.Type, "[]") {
|
|
||||||
return strings.TrimSuffix(t.Type, "[]")
|
|
||||||
}
|
|
||||||
return t.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
type Types map[string][]Type
|
|
||||||
|
|
||||||
type TypePriority struct {
|
|
||||||
Type string
|
|
||||||
Value uint
|
|
||||||
}
|
|
||||||
|
|
||||||
type TypedDataMessage = map[string]interface{}
|
|
||||||
|
|
||||||
// TypedDataDomain represents the domain part of an EIP-712 message.
|
|
||||||
type TypedDataDomain struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
ChainId *math.HexOrDecimal256 `json:"chainId"`
|
|
||||||
VerifyingContract string `json:"verifyingContract"`
|
|
||||||
Salt string `json:"salt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypedDataAndHash is a helper function that calculates a hash for typed data conforming to EIP-712.
|
|
||||||
// This hash can then be safely used to calculate a signature.
|
|
||||||
//
|
|
||||||
// See https://eips.ethereum.org/EIPS/eip-712 for the full specification.
|
|
||||||
//
|
|
||||||
// This gives context to the signed typed data and prevents signing of transactions.
|
|
||||||
func TypedDataAndHash(typedData TypedData) ([]byte, string, error) {
|
|
||||||
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
rawData := fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))
|
|
||||||
return crypto.Keccak256([]byte(rawData)), rawData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashStruct generates a keccak256 hash of the encoding of the provided data
|
|
||||||
func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) {
|
|
||||||
encodedData, err := typedData.EncodeData(primaryType, data, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return crypto.Keccak256(encodedData), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies returns an array of custom types ordered by their hierarchical reference tree
|
|
||||||
func (typedData *TypedData) Dependencies(primaryType string, found []string) []string {
|
|
||||||
primaryType = strings.TrimSuffix(primaryType, "[]")
|
|
||||||
includes := func(arr []string, str string) bool {
|
|
||||||
for _, obj := range arr {
|
|
||||||
if obj == str {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if includes(found, primaryType) {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
if typedData.Types[primaryType] == nil {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
found = append(found, primaryType)
|
|
||||||
for _, field := range typedData.Types[primaryType] {
|
|
||||||
for _, dep := range typedData.Dependencies(field.Type, found) {
|
|
||||||
if !includes(found, dep) {
|
|
||||||
found = append(found, dep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeType generates the following encoding:
|
|
||||||
// `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"`
|
|
||||||
//
|
|
||||||
// each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name
|
|
||||||
func (typedData *TypedData) EncodeType(primaryType string) hexutil.Bytes {
|
|
||||||
// Get dependencies primary first, then alphabetical
|
|
||||||
deps := typedData.Dependencies(primaryType, []string{})
|
|
||||||
if len(deps) > 0 {
|
|
||||||
slicedDeps := deps[1:]
|
|
||||||
sort.Strings(slicedDeps)
|
|
||||||
deps = append([]string{primaryType}, slicedDeps...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format as a string with fields
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
for _, dep := range deps {
|
|
||||||
buffer.WriteString(dep)
|
|
||||||
buffer.WriteString("(")
|
|
||||||
for _, obj := range typedData.Types[dep] {
|
|
||||||
buffer.WriteString(obj.Type)
|
|
||||||
buffer.WriteString(" ")
|
|
||||||
buffer.WriteString(obj.Name)
|
|
||||||
buffer.WriteString(",")
|
|
||||||
}
|
|
||||||
buffer.Truncate(buffer.Len() - 1)
|
|
||||||
buffer.WriteString(")")
|
|
||||||
}
|
|
||||||
return buffer.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeHash creates the keccak256 hash of the data
|
|
||||||
func (typedData *TypedData) TypeHash(primaryType string) hexutil.Bytes {
|
|
||||||
return crypto.Keccak256(typedData.EncodeType(primaryType))
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeData generates the following encoding:
|
|
||||||
// `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`
|
|
||||||
//
|
|
||||||
// each encoded member is 32-byte long
|
|
||||||
func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) {
|
|
||||||
if err := typedData.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := bytes.Buffer{}
|
|
||||||
|
|
||||||
// Verify extra data
|
|
||||||
if exp, got := len(typedData.Types[primaryType]), len(data); exp < got {
|
|
||||||
return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add typehash
|
|
||||||
buffer.Write(typedData.TypeHash(primaryType))
|
|
||||||
|
|
||||||
// Add field contents. Structs and arrays have special handlers.
|
|
||||||
for _, field := range typedData.Types[primaryType] {
|
|
||||||
encType := field.Type
|
|
||||||
encValue := data[field.Name]
|
|
||||||
if encType[len(encType)-1:] == "]" {
|
|
||||||
arrayValue, err := convertDataToSlice(encValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
arrayBuffer := bytes.Buffer{}
|
|
||||||
parsedType := strings.Split(encType, "[")[0]
|
|
||||||
for _, item := range arrayValue {
|
|
||||||
if typedData.Types[parsedType] != nil {
|
|
||||||
mapValue, ok := item.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil, dataMismatchError(parsedType, item)
|
|
||||||
}
|
|
||||||
encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
arrayBuffer.Write(crypto.Keccak256(encodedData))
|
|
||||||
} else {
|
|
||||||
bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
arrayBuffer.Write(bytesValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.Write(crypto.Keccak256(arrayBuffer.Bytes()))
|
|
||||||
} else if typedData.Types[field.Type] != nil {
|
|
||||||
mapValue, ok := encValue.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
}
|
|
||||||
encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(crypto.Keccak256(encodedData))
|
|
||||||
} else {
|
|
||||||
byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(byteValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to parse bytes in different formats: byte array, hex string, hexutil.Bytes.
|
|
||||||
func parseBytes(encType interface{}) ([]byte, bool) {
|
|
||||||
// Handle array types.
|
|
||||||
val := reflect.ValueOf(encType)
|
|
||||||
if val.Kind() == reflect.Array && val.Type().Elem().Kind() == reflect.Uint8 {
|
|
||||||
v := reflect.MakeSlice(reflect.TypeOf([]byte{}), val.Len(), val.Len())
|
|
||||||
reflect.Copy(v, val)
|
|
||||||
return v.Bytes(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := encType.(type) {
|
|
||||||
case []byte:
|
|
||||||
return v, true
|
|
||||||
case hexutil.Bytes:
|
|
||||||
return v, true
|
|
||||||
case string:
|
|
||||||
bytes, err := hexutil.Decode(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return bytes, true
|
|
||||||
default:
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInteger(encType string, encValue interface{}) (*big.Int, error) {
|
|
||||||
var (
|
|
||||||
length int
|
|
||||||
signed = strings.HasPrefix(encType, "int")
|
|
||||||
b *big.Int
|
|
||||||
)
|
|
||||||
if encType == "int" || encType == "uint" {
|
|
||||||
length = 256
|
|
||||||
} else {
|
|
||||||
lengthStr := ""
|
|
||||||
if strings.HasPrefix(encType, "uint") {
|
|
||||||
lengthStr = strings.TrimPrefix(encType, "uint")
|
|
||||||
} else {
|
|
||||||
lengthStr = strings.TrimPrefix(encType, "int")
|
|
||||||
}
|
|
||||||
atoiSize, err := strconv.Atoi(lengthStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid size on integer: %v", lengthStr)
|
|
||||||
}
|
|
||||||
length = atoiSize
|
|
||||||
}
|
|
||||||
switch v := encValue.(type) {
|
|
||||||
case *math.HexOrDecimal256:
|
|
||||||
b = (*big.Int)(v)
|
|
||||||
case *big.Int:
|
|
||||||
b = v
|
|
||||||
case string:
|
|
||||||
var hexIntValue math.HexOrDecimal256
|
|
||||||
if err := hexIntValue.UnmarshalText([]byte(v)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b = (*big.Int)(&hexIntValue)
|
|
||||||
case float64:
|
|
||||||
// JSON parses non-strings as float64. Fail if we cannot
|
|
||||||
// convert it losslessly
|
|
||||||
if float64(int64(v)) == v {
|
|
||||||
b = big.NewInt(int64(v))
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("invalid float value %v for type %v", v, encType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("invalid integer value %v/%v for type %v", encValue, reflect.TypeOf(encValue), encType)
|
|
||||||
}
|
|
||||||
if b.BitLen() > length {
|
|
||||||
return nil, fmt.Errorf("integer larger than '%v'", encType)
|
|
||||||
}
|
|
||||||
if !signed && b.Sign() == -1 {
|
|
||||||
return nil, fmt.Errorf("invalid negative value for unsigned type %v", encType)
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodePrimitiveValue deals with the primitive values found
|
|
||||||
// while searching through the typed data
|
|
||||||
func (typedData *TypedData) EncodePrimitiveValue(encType string, encValue interface{}, depth int) ([]byte, error) {
|
|
||||||
switch encType {
|
|
||||||
case "address":
|
|
||||||
retval := make([]byte, 32)
|
|
||||||
switch val := encValue.(type) {
|
|
||||||
case string:
|
|
||||||
if common.IsHexAddress(val) {
|
|
||||||
copy(retval[12:], common.HexToAddress(val).Bytes())
|
|
||||||
return retval, nil
|
|
||||||
}
|
|
||||||
case []byte:
|
|
||||||
if len(val) == 20 {
|
|
||||||
copy(retval[12:], val)
|
|
||||||
return retval, nil
|
|
||||||
}
|
|
||||||
case [20]byte:
|
|
||||||
copy(retval[12:], val[:])
|
|
||||||
return retval, nil
|
|
||||||
}
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
case "bool":
|
|
||||||
boolValue, ok := encValue.(bool)
|
|
||||||
if !ok {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
}
|
|
||||||
if boolValue {
|
|
||||||
return math.PaddedBigBytes(common.Big1, 32), nil
|
|
||||||
}
|
|
||||||
return math.PaddedBigBytes(common.Big0, 32), nil
|
|
||||||
case "string":
|
|
||||||
strVal, ok := encValue.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
}
|
|
||||||
return crypto.Keccak256([]byte(strVal)), nil
|
|
||||||
case "bytes":
|
|
||||||
bytesValue, ok := parseBytes(encValue)
|
|
||||||
if !ok {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
}
|
|
||||||
return crypto.Keccak256(bytesValue), nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(encType, "bytes") {
|
|
||||||
lengthStr := strings.TrimPrefix(encType, "bytes")
|
|
||||||
length, err := strconv.Atoi(lengthStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid size on bytes: %v", lengthStr)
|
|
||||||
}
|
|
||||||
if length < 0 || length > 32 {
|
|
||||||
return nil, fmt.Errorf("invalid size on bytes: %d", length)
|
|
||||||
}
|
|
||||||
if byteValue, ok := parseBytes(encValue); !ok || len(byteValue) != length {
|
|
||||||
return nil, dataMismatchError(encType, encValue)
|
|
||||||
} else {
|
|
||||||
// Right-pad the bits
|
|
||||||
dst := make([]byte, 32)
|
|
||||||
copy(dst, byteValue)
|
|
||||||
return dst, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") {
|
|
||||||
b, err := parseInteger(encType, encValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return math.U256Bytes(b), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unrecognized type '%s'", encType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dataMismatchError generates an error for a mismatch between
|
|
||||||
// the provided type and data
|
|
||||||
func dataMismatchError(encType string, encValue interface{}) error {
|
|
||||||
return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertDataToSlice(encValue interface{}) ([]interface{}, error) {
|
|
||||||
var outEncValue []interface{}
|
|
||||||
rv := reflect.ValueOf(encValue)
|
|
||||||
if rv.Kind() == reflect.Slice {
|
|
||||||
for i := 0; i < rv.Len(); i++ {
|
|
||||||
outEncValue = append(outEncValue, rv.Index(i).Interface())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return outEncValue, fmt.Errorf("provided data '%v' is not slice", encValue)
|
|
||||||
}
|
|
||||||
return outEncValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate makes sure the types are sound
|
|
||||||
func (typedData *TypedData) validate() error {
|
|
||||||
if err := typedData.Types.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := typedData.Domain.validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map generates a map version of the typed data
|
|
||||||
func (typedData *TypedData) Map() map[string]interface{} {
|
|
||||||
dataMap := map[string]interface{}{
|
|
||||||
"types": typedData.Types,
|
|
||||||
"domain": typedData.Domain.Map(),
|
|
||||||
"primaryType": typedData.PrimaryType,
|
|
||||||
"message": typedData.Message,
|
|
||||||
}
|
|
||||||
return dataMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format returns a representation of typedData, which can be easily displayed by a user-interface
|
|
||||||
// without in-depth knowledge about 712 rules
|
|
||||||
func (typedData *TypedData) Format() ([]*NameValueType, error) {
|
|
||||||
domain, err := typedData.formatData("EIP712Domain", typedData.Domain.Map())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ptype, err := typedData.formatData(typedData.PrimaryType, typedData.Message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var nvts []*NameValueType
|
|
||||||
nvts = append(nvts, &NameValueType{
|
|
||||||
Name: "EIP712Domain",
|
|
||||||
Value: domain,
|
|
||||||
Typ: "domain",
|
|
||||||
})
|
|
||||||
nvts = append(nvts, &NameValueType{
|
|
||||||
Name: typedData.PrimaryType,
|
|
||||||
Value: ptype,
|
|
||||||
Typ: "primary type",
|
|
||||||
})
|
|
||||||
return nvts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) ([]*NameValueType, error) {
|
|
||||||
var output []*NameValueType
|
|
||||||
|
|
||||||
// Add field contents. Structs and arrays have special handlers.
|
|
||||||
for _, field := range typedData.Types[primaryType] {
|
|
||||||
encName := field.Name
|
|
||||||
encValue := data[encName]
|
|
||||||
item := &NameValueType{
|
|
||||||
Name: encName,
|
|
||||||
Typ: field.Type,
|
|
||||||
}
|
|
||||||
if field.isArray() {
|
|
||||||
arrayValue, _ := convertDataToSlice(encValue)
|
|
||||||
parsedType := field.typeName()
|
|
||||||
for _, v := range arrayValue {
|
|
||||||
if typedData.Types[parsedType] != nil {
|
|
||||||
mapValue, _ := v.(map[string]interface{})
|
|
||||||
mapOutput, err := typedData.formatData(parsedType, mapValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Value = mapOutput
|
|
||||||
} else {
|
|
||||||
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Value = primitiveOutput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if typedData.Types[field.Type] != nil {
|
|
||||||
if mapValue, ok := encValue.(map[string]interface{}); ok {
|
|
||||||
mapOutput, err := typedData.formatData(field.Type, mapValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Value = mapOutput
|
|
||||||
} else {
|
|
||||||
item.Value = "<nil>"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Value = primitiveOutput
|
|
||||||
}
|
|
||||||
output = append(output, item)
|
|
||||||
}
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatPrimitiveValue(encType string, encValue interface{}) (string, error) {
|
|
||||||
switch encType {
|
|
||||||
case "address":
|
|
||||||
if stringValue, ok := encValue.(string); !ok {
|
|
||||||
return "", fmt.Errorf("could not format value %v as address", encValue)
|
|
||||||
} else {
|
|
||||||
return common.HexToAddress(stringValue).String(), nil
|
|
||||||
}
|
|
||||||
case "bool":
|
|
||||||
if boolValue, ok := encValue.(bool); !ok {
|
|
||||||
return "", fmt.Errorf("could not format value %v as bool", encValue)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%t", boolValue), nil
|
|
||||||
}
|
|
||||||
case "bytes", "string":
|
|
||||||
return fmt.Sprintf("%s", encValue), nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(encType, "bytes") {
|
|
||||||
return fmt.Sprintf("%s", encValue), nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") {
|
|
||||||
if b, err := parseInteger(encType, encValue); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%d (%#x)", b, b), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("unhandled type %v", encType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks if the types object is conformant to the specs
|
|
||||||
func (t Types) validate() error {
|
|
||||||
for typeKey, typeArr := range t {
|
|
||||||
if len(typeKey) == 0 {
|
|
||||||
return fmt.Errorf("empty type key")
|
|
||||||
}
|
|
||||||
for i, typeObj := range typeArr {
|
|
||||||
if len(typeObj.Type) == 0 {
|
|
||||||
return fmt.Errorf("type %q:%d: empty Type", typeKey, i)
|
|
||||||
}
|
|
||||||
if len(typeObj.Name) == 0 {
|
|
||||||
return fmt.Errorf("type %q:%d: empty Name", typeKey, i)
|
|
||||||
}
|
|
||||||
if typeKey == typeObj.Type {
|
|
||||||
return fmt.Errorf("type %q cannot reference itself", typeObj.Type)
|
|
||||||
}
|
|
||||||
if isPrimitiveTypeValid(typeObj.Type) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Must be reference type
|
|
||||||
if _, exist := t[typeObj.typeName()]; !exist {
|
|
||||||
return fmt.Errorf("reference type %q is undefined", typeObj.Type)
|
|
||||||
}
|
|
||||||
if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) {
|
|
||||||
return fmt.Errorf("unknown reference type %q", typeObj.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the primitive value is valid
|
|
||||||
func isPrimitiveTypeValid(primitiveType string) bool {
|
|
||||||
if primitiveType == "address" ||
|
|
||||||
primitiveType == "address[]" ||
|
|
||||||
primitiveType == "bool" ||
|
|
||||||
primitiveType == "bool[]" ||
|
|
||||||
primitiveType == "string" ||
|
|
||||||
primitiveType == "string[]" ||
|
|
||||||
primitiveType == "bytes" ||
|
|
||||||
primitiveType == "bytes[]" ||
|
|
||||||
primitiveType == "int" ||
|
|
||||||
primitiveType == "int[]" ||
|
|
||||||
primitiveType == "uint" ||
|
|
||||||
primitiveType == "uint[]" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// For 'bytesN', 'bytesN[]', we allow N from 1 to 32
|
|
||||||
for n := 1; n <= 32; n++ {
|
|
||||||
// e.g. 'bytes28' or 'bytes28[]'
|
|
||||||
if primitiveType == fmt.Sprintf("bytes%d", n) || primitiveType == fmt.Sprintf("bytes%d[]", n) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For 'intN','intN[]' and 'uintN','uintN[]' we allow N in increments of 8, from 8 up to 256
|
|
||||||
for n := 8; n <= 256; n += 8 {
|
|
||||||
if primitiveType == fmt.Sprintf("int%d", n) || primitiveType == fmt.Sprintf("int%d[]", n) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if primitiveType == fmt.Sprintf("uint%d", n) || primitiveType == fmt.Sprintf("uint%d[]", n) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate checks if the given domain is valid, i.e. contains at least
|
|
||||||
// the minimum viable keys and values
|
|
||||||
func (domain *TypedDataDomain) validate() error {
|
|
||||||
if domain.ChainId == nil && len(domain.Name) == 0 && len(domain.Version) == 0 && len(domain.VerifyingContract) == 0 && len(domain.Salt) == 0 {
|
|
||||||
return errors.New("domain is undefined")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map is a helper function to generate a map version of the domain
|
|
||||||
func (domain *TypedDataDomain) Map() map[string]interface{} {
|
|
||||||
dataMap := map[string]interface{}{}
|
|
||||||
|
|
||||||
if domain.ChainId != nil {
|
|
||||||
dataMap["chainId"] = domain.ChainId
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(domain.Name) > 0 {
|
|
||||||
dataMap["name"] = domain.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(domain.Version) > 0 {
|
|
||||||
dataMap["version"] = domain.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(domain.VerifyingContract) > 0 {
|
|
||||||
dataMap["verifyingContract"] = domain.VerifyingContract
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(domain.Salt) > 0 {
|
|
||||||
dataMap["salt"] = domain.Salt
|
|
||||||
}
|
|
||||||
return dataMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// NameValueType is a very simple struct with Name, Value and Type. It's meant for simple
|
|
||||||
// json structures used to communicate signing-info about typed data with the UI
|
|
||||||
type NameValueType struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value interface{} `json:"value"`
|
|
||||||
Typ string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pprint returns a pretty-printed version of nvt
|
|
||||||
func (nvt *NameValueType) Pprint(depth int) string {
|
|
||||||
output := bytes.Buffer{}
|
|
||||||
output.WriteString(strings.Repeat("\u00a0", depth*2))
|
|
||||||
output.WriteString(fmt.Sprintf("%s [%s]: ", nvt.Name, nvt.Typ))
|
|
||||||
if nvts, ok := nvt.Value.([]*NameValueType); ok {
|
|
||||||
output.WriteString("\n")
|
|
||||||
for _, next := range nvts {
|
|
||||||
sublevel := next.Pprint(depth + 1)
|
|
||||||
output.WriteString(sublevel)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if nvt.Value != nil {
|
|
||||||
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
|
|
||||||
} else {
|
|
||||||
output.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.String()
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Copyright 2023 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 apitypes
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestIsPrimitive(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// Expected positives
|
|
||||||
for i, tc := range []string{
|
|
||||||
"int24", "int24[]", "uint88", "uint88[]", "uint", "uint[]", "int256", "int256[]",
|
|
||||||
"uint96", "uint96[]", "int96", "int96[]", "bytes17[]", "bytes17",
|
|
||||||
} {
|
|
||||||
if !isPrimitiveTypeValid(tc) {
|
|
||||||
t.Errorf("test %d: expected '%v' to be a valid primitive", i, tc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Expected negatives
|
|
||||||
for i, tc := range []string{
|
|
||||||
"int257", "int257[]", "uint88 ", "uint88 []", "uint257", "uint-1[]",
|
|
||||||
"uint0", "uint0[]", "int95", "int95[]", "uint1", "uint1[]", "bytes33[]", "bytess",
|
|
||||||
} {
|
|
||||||
if isPrimitiveTypeValid(tc) {
|
|
||||||
t.Errorf("test %d: expected '%v' to not be a valid primitive", i, tc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuditLogger struct {
|
|
||||||
log log.Logger
|
|
||||||
api ExternalAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) List(ctx context.Context) ([]common.Address, error) {
|
|
||||||
l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String())
|
|
||||||
res, e := l.api.List(ctx)
|
|
||||||
l.log.Info("List", "type", "response", "data", res)
|
|
||||||
|
|
||||||
return res, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) New(ctx context.Context) (common.Address, error) {
|
|
||||||
return l.api.New(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
|
|
||||||
sel := "<nil>"
|
|
||||||
if methodSelector != nil {
|
|
||||||
sel = *methodSelector
|
|
||||||
}
|
|
||||||
l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
|
||||||
"tx", args.String(),
|
|
||||||
"methodSelector", sel)
|
|
||||||
|
|
||||||
res, e := l.api.SignTransaction(ctx, args, methodSelector)
|
|
||||||
if res != nil {
|
|
||||||
l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e)
|
|
||||||
} else {
|
|
||||||
l.log.Info("SignTransaction", "type", "response", "data", res, "error", e)
|
|
||||||
}
|
|
||||||
return res, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
|
|
||||||
marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
|
|
||||||
l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
|
||||||
"addr", addr.String(), "data", marshalledData, "content-type", contentType)
|
|
||||||
b, e := l.api.SignData(ctx, contentType, addr, data)
|
|
||||||
l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
|
|
||||||
return b, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
|
|
||||||
sel := "<nil>"
|
|
||||||
if methodSelector != nil {
|
|
||||||
sel = *methodSelector
|
|
||||||
}
|
|
||||||
data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
|
|
||||||
l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
|
||||||
"addr", addr.String(), "data", string(data), "selector", sel)
|
|
||||||
res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector)
|
|
||||||
if res != nil {
|
|
||||||
data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
|
|
||||||
l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e)
|
|
||||||
} else {
|
|
||||||
l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e)
|
|
||||||
}
|
|
||||||
return res, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error) {
|
|
||||||
l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
|
||||||
"addr", addr.String(), "data", data)
|
|
||||||
b, e := l.api.SignTypedData(ctx, addr, data)
|
|
||||||
l.log.Info("SignTypedData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
|
|
||||||
return b, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
|
|
||||||
l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(),
|
|
||||||
"data", common.Bytes2Hex(data), "sig", common.Bytes2Hex(sig))
|
|
||||||
b, e := l.api.EcRecover(ctx, data, sig)
|
|
||||||
l.log.Info("EcRecover", "type", "response", "address", b.String(), "error", e)
|
|
||||||
return b, e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AuditLogger) Version(ctx context.Context) (string, error) {
|
|
||||||
l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String())
|
|
||||||
data, err := l.api.Version(ctx)
|
|
||||||
l.log.Info("Version", "type", "response", "data", data, "error", err)
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
|
|
||||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := slog.NewTextHandler(f, nil)
|
|
||||||
l := log.NewLogger(handler).With("api", "signer")
|
|
||||||
l.Info("Configured", "audit log", path)
|
|
||||||
return &AuditLogger{l, api}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/console/prompt"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CommandlineUI struct {
|
|
||||||
in *bufio.Reader
|
|
||||||
mu sync.Mutex
|
|
||||||
api *UIServerAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommandlineUI() *CommandlineUI {
|
|
||||||
return &CommandlineUI{in: bufio.NewReader(os.Stdin)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *CommandlineUI) RegisterUIServer(api *UIServerAPI) {
|
|
||||||
ui.api = api
|
|
||||||
}
|
|
||||||
|
|
||||||
// readString reads a single line from stdin, trimming if from spaces, enforcing
|
|
||||||
// non-emptyness.
|
|
||||||
func (ui *CommandlineUI) readString() string {
|
|
||||||
for {
|
|
||||||
fmt.Printf("> ")
|
|
||||||
text, err := ui.in.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to read user input", "err", err)
|
|
||||||
}
|
|
||||||
if text = strings.TrimSpace(text); text != "" {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
|
||||||
fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt)
|
|
||||||
defer fmt.Println("-----------------------")
|
|
||||||
if info.IsPassword {
|
|
||||||
text, err := prompt.Stdin.PromptPassword("> ")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read password", "error", err)
|
|
||||||
return UserInputResponse{}, err
|
|
||||||
}
|
|
||||||
return UserInputResponse{text}, nil
|
|
||||||
}
|
|
||||||
text := ui.readString()
|
|
||||||
return UserInputResponse{text}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// confirm returns true if user enters 'Yes', otherwise false
|
|
||||||
func (ui *CommandlineUI) confirm() bool {
|
|
||||||
fmt.Printf("Approve? [y/N]:\n")
|
|
||||||
if ui.readString() == "y" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
fmt.Println("-----------------------")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize quotes and truncates 'txt' if longer than 'limit'. If truncated,
|
|
||||||
// and ellipsis is added after the quoted string
|
|
||||||
func sanitize(txt string, limit int) string {
|
|
||||||
if len(txt) > limit {
|
|
||||||
return fmt.Sprintf("%q...", txt[:limit])
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%q", txt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showMetadata(metadata Metadata) {
|
|
||||||
fmt.Printf("Request context:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local)
|
|
||||||
fmt.Printf("\nAdditional HTTP header data, provided by the external caller:\n")
|
|
||||||
fmt.Printf("\tUser-Agent: %v\n\tOrigin: %v\n", sanitize(metadata.UserAgent, 200), sanitize(metadata.Origin, 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
|
||||||
func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
|
|
||||||
ui.mu.Lock()
|
|
||||||
defer ui.mu.Unlock()
|
|
||||||
weival := request.Transaction.Value.ToInt()
|
|
||||||
fmt.Printf("--------- Transaction request-------------\n")
|
|
||||||
if to := request.Transaction.To; to != nil {
|
|
||||||
fmt.Printf("to: %v\n", to.Original())
|
|
||||||
if !to.ValidChecksum() {
|
|
||||||
fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("to: <contact creation>\n")
|
|
||||||
}
|
|
||||||
fmt.Printf("from: %v\n", request.Transaction.From.String())
|
|
||||||
fmt.Printf("value: %v wei\n", weival)
|
|
||||||
fmt.Printf("gas: %v (%v)\n", request.Transaction.Gas, uint64(request.Transaction.Gas))
|
|
||||||
if request.Transaction.MaxFeePerGas != nil {
|
|
||||||
fmt.Printf("maxFeePerGas: %v wei\n", request.Transaction.MaxFeePerGas.ToInt())
|
|
||||||
fmt.Printf("maxPriorityFeePerGas: %v wei\n", request.Transaction.MaxPriorityFeePerGas.ToInt())
|
|
||||||
} else {
|
|
||||||
fmt.Printf("gasprice: %v wei\n", request.Transaction.GasPrice.ToInt())
|
|
||||||
}
|
|
||||||
fmt.Printf("nonce: %v (%v)\n", request.Transaction.Nonce, uint64(request.Transaction.Nonce))
|
|
||||||
if chainId := request.Transaction.ChainID; chainId != nil {
|
|
||||||
fmt.Printf("chainid: %v\n", chainId)
|
|
||||||
}
|
|
||||||
if list := request.Transaction.AccessList; list != nil {
|
|
||||||
fmt.Printf("Accesslist\n")
|
|
||||||
for i, el := range *list {
|
|
||||||
fmt.Printf(" %d. %v\n", i, el.Address)
|
|
||||||
for j, slot := range el.StorageKeys {
|
|
||||||
fmt.Printf(" %d. %v\n", j, slot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if request.Transaction.Data != nil {
|
|
||||||
d := *request.Transaction.Data
|
|
||||||
if len(d) > 0 {
|
|
||||||
fmt.Printf("data: %v\n", hexutil.Encode(d))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if request.Callinfo != nil {
|
|
||||||
fmt.Printf("\nTransaction validation:\n")
|
|
||||||
for _, m := range request.Callinfo {
|
|
||||||
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
showMetadata(request.Meta)
|
|
||||||
fmt.Printf("-------------------------------------------\n")
|
|
||||||
if !ui.confirm() {
|
|
||||||
return SignTxResponse{request.Transaction, false}, nil
|
|
||||||
}
|
|
||||||
return SignTxResponse{request.Transaction, true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveSignData prompt the user for confirmation to request to sign data
|
|
||||||
func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
|
|
||||||
ui.mu.Lock()
|
|
||||||
defer ui.mu.Unlock()
|
|
||||||
|
|
||||||
fmt.Printf("-------- Sign data request--------------\n")
|
|
||||||
fmt.Printf("Account: %s\n", request.Address.String())
|
|
||||||
if len(request.Callinfo) != 0 {
|
|
||||||
fmt.Printf("\nValidation messages:\n")
|
|
||||||
for _, m := range request.Callinfo {
|
|
||||||
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
fmt.Printf("messages:\n")
|
|
||||||
for _, nvt := range request.Messages {
|
|
||||||
fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1)))
|
|
||||||
}
|
|
||||||
fmt.Printf("raw data: \n\t%q\n", request.Rawdata)
|
|
||||||
fmt.Printf("data hash: %v\n", request.Hash)
|
|
||||||
fmt.Printf("-------------------------------------------\n")
|
|
||||||
showMetadata(request.Meta)
|
|
||||||
if !ui.confirm() {
|
|
||||||
return SignDataResponse{false}, nil
|
|
||||||
}
|
|
||||||
return SignDataResponse{true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveListing prompt the user for confirmation to list accounts
|
|
||||||
// the list of accounts to list can be modified by the UI
|
|
||||||
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
|
|
||||||
ui.mu.Lock()
|
|
||||||
defer ui.mu.Unlock()
|
|
||||||
|
|
||||||
fmt.Printf("-------- List Account request--------------\n")
|
|
||||||
fmt.Printf("A request has been made to list all accounts. \n")
|
|
||||||
fmt.Printf("You can select which accounts the caller can see\n")
|
|
||||||
for _, account := range request.Accounts {
|
|
||||||
fmt.Printf(" [x] %v\n", account.Address.Hex())
|
|
||||||
fmt.Printf(" URL: %v\n", account.URL)
|
|
||||||
}
|
|
||||||
fmt.Printf("-------------------------------------------\n")
|
|
||||||
showMetadata(request.Meta)
|
|
||||||
if !ui.confirm() {
|
|
||||||
return ListResponse{nil}, nil
|
|
||||||
}
|
|
||||||
return ListResponse{request.Accounts}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
|
|
||||||
func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
|
|
||||||
ui.mu.Lock()
|
|
||||||
defer ui.mu.Unlock()
|
|
||||||
|
|
||||||
fmt.Printf("-------- New Account request--------------\n\n")
|
|
||||||
fmt.Printf("A request has been made to create a new account. \n")
|
|
||||||
fmt.Printf("Approving this operation means that a new account is created,\n")
|
|
||||||
fmt.Printf("and the address is returned to the external caller\n\n")
|
|
||||||
showMetadata(request.Meta)
|
|
||||||
if !ui.confirm() {
|
|
||||||
return NewAccountResponse{false}, nil
|
|
||||||
}
|
|
||||||
return NewAccountResponse{true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowError displays error message to user
|
|
||||||
func (ui *CommandlineUI) ShowError(message string) {
|
|
||||||
fmt.Printf("## Error \n%s\n", message)
|
|
||||||
fmt.Printf("-------------------------------------------\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowInfo displays info message to user
|
|
||||||
func (ui *CommandlineUI) ShowInfo(message string) {
|
|
||||||
fmt.Printf("## Info \n%s\n", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
fmt.Printf("Transaction signed:\n ")
|
|
||||||
if jsn, err := json.MarshalIndent(tx.Tx, " ", " "); err != nil {
|
|
||||||
fmt.Printf("WARN: marshalling error %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(string(jsn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *CommandlineUI) showAccounts() {
|
|
||||||
accounts, err := ui.api.ListAccounts(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error listing accounts", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(accounts) == 0 {
|
|
||||||
fmt.Print("No accounts found\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
var out = new(strings.Builder)
|
|
||||||
if limit := 20; len(accounts) > limit {
|
|
||||||
msg = fmt.Sprintf("\nFirst %d accounts listed (%d more available).\n", limit, len(accounts)-limit)
|
|
||||||
accounts = accounts[:limit]
|
|
||||||
}
|
|
||||||
fmt.Fprint(out, "\n------- Available accounts -------\n")
|
|
||||||
for i, account := range accounts {
|
|
||||||
fmt.Fprintf(out, "%d. %s at %s\n", i, account.Address, account.URL)
|
|
||||||
}
|
|
||||||
fmt.Print(out.String(), msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) {
|
|
||||||
fmt.Print("\n------- Signer info -------\n")
|
|
||||||
for k, v := range info.Info {
|
|
||||||
fmt.Printf("* %v : %v\n", k, v)
|
|
||||||
}
|
|
||||||
go ui.showAccounts()
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
|
|
||||||
// it also conforms to the API required by the Gnosis Safe tx relay service.
|
|
||||||
// See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
|
|
||||||
type GnosisSafeTx struct {
|
|
||||||
// These fields are only used on output
|
|
||||||
Signature hexutil.Bytes `json:"signature"`
|
|
||||||
SafeTxHash common.Hash `json:"contractTransactionHash"`
|
|
||||||
Sender common.MixedcaseAddress `json:"sender"`
|
|
||||||
// These fields are used both on input and output
|
|
||||||
Safe common.MixedcaseAddress `json:"safe"`
|
|
||||||
To common.MixedcaseAddress `json:"to"`
|
|
||||||
Value math.Decimal256 `json:"value"`
|
|
||||||
GasPrice math.Decimal256 `json:"gasPrice"`
|
|
||||||
Data *hexutil.Bytes `json:"data"`
|
|
||||||
Operation uint8 `json:"operation"`
|
|
||||||
GasToken common.Address `json:"gasToken"`
|
|
||||||
RefundReceiver common.Address `json:"refundReceiver"`
|
|
||||||
BaseGas big.Int `json:"baseGas"`
|
|
||||||
SafeTxGas big.Int `json:"safeTxGas"`
|
|
||||||
Nonce big.Int `json:"nonce"`
|
|
||||||
InputExpHash common.Hash `json:"safeTxHash"`
|
|
||||||
ChainId *math.HexOrDecimal256 `json:"chainId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
|
|
||||||
func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData {
|
|
||||||
var data hexutil.Bytes
|
|
||||||
if tx.Data != nil {
|
|
||||||
data = *tx.Data
|
|
||||||
}
|
|
||||||
var domainType = []apitypes.Type{{Name: "verifyingContract", Type: "address"}}
|
|
||||||
if tx.ChainId != nil {
|
|
||||||
domainType = append([]apitypes.Type{{Name: "chainId", Type: "uint256"}}, domainType[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
gnosisTypedData := apitypes.TypedData{
|
|
||||||
Types: apitypes.Types{
|
|
||||||
"EIP712Domain": domainType,
|
|
||||||
"SafeTx": []apitypes.Type{
|
|
||||||
{Name: "to", Type: "address"},
|
|
||||||
{Name: "value", Type: "uint256"},
|
|
||||||
{Name: "data", Type: "bytes"},
|
|
||||||
{Name: "operation", Type: "uint8"},
|
|
||||||
{Name: "safeTxGas", Type: "uint256"},
|
|
||||||
{Name: "baseGas", Type: "uint256"},
|
|
||||||
{Name: "gasPrice", Type: "uint256"},
|
|
||||||
{Name: "gasToken", Type: "address"},
|
|
||||||
{Name: "refundReceiver", Type: "address"},
|
|
||||||
{Name: "nonce", Type: "uint256"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Domain: apitypes.TypedDataDomain{
|
|
||||||
VerifyingContract: tx.Safe.Address().Hex(),
|
|
||||||
ChainId: tx.ChainId,
|
|
||||||
},
|
|
||||||
PrimaryType: "SafeTx",
|
|
||||||
Message: apitypes.TypedDataMessage{
|
|
||||||
"to": tx.To.Address().Hex(),
|
|
||||||
"value": tx.Value.String(),
|
|
||||||
"data": data,
|
|
||||||
"operation": fmt.Sprintf("%d", tx.Operation),
|
|
||||||
"safeTxGas": fmt.Sprintf("%#d", &tx.SafeTxGas),
|
|
||||||
"baseGas": fmt.Sprintf("%#d", &tx.BaseGas),
|
|
||||||
"gasPrice": tx.GasPrice.String(),
|
|
||||||
"gasToken": tx.GasToken.Hex(),
|
|
||||||
"refundReceiver": tx.RefundReceiver.Hex(),
|
|
||||||
"nonce": fmt.Sprintf("%d", tx.Nonce.Uint64()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return gnosisTypedData
|
|
||||||
}
|
|
||||||
|
|
||||||
// ArgsForValidation returns a SendTxArgs struct, which can be used for the
|
|
||||||
// common validations, e.g. look up 4byte destinations
|
|
||||||
func (tx *GnosisSafeTx) ArgsForValidation() *apitypes.SendTxArgs {
|
|
||||||
gp := hexutil.Big(tx.GasPrice)
|
|
||||||
args := &apitypes.SendTxArgs{
|
|
||||||
From: tx.Safe,
|
|
||||||
To: &tx.To,
|
|
||||||
Gas: hexutil.Uint64(tx.SafeTxGas.Uint64()),
|
|
||||||
GasPrice: &gp,
|
|
||||||
Value: hexutil.Big(tx.Value),
|
|
||||||
Nonce: hexutil.Uint64(tx.Nonce.Uint64()),
|
|
||||||
Data: tx.Data,
|
|
||||||
Input: nil,
|
|
||||||
ChainID: (*hexutil.Big)(tx.ChainId),
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
@ -1,345 +0,0 @@
|
||||||
// Copyright 2019 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"mime"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/clique"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sign receives a request and produces a signature
|
|
||||||
//
|
|
||||||
// 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, if legacyV==true.
|
|
||||||
func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
|
|
||||||
// We make the request prior to looking up if we actually have the account, to prevent
|
|
||||||
// account-enumeration via the API
|
|
||||||
res, err := api.UI.ApproveSignData(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !res.Approved {
|
|
||||||
return nil, ErrRequestDenied
|
|
||||||
}
|
|
||||||
// Look up the wallet containing the requested signer
|
|
||||||
account := accounts.Account{Address: req.Address.Address()}
|
|
||||||
wallet, err := api.am.Find(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pw, err := api.lookupOrQueryPassword(account.Address,
|
|
||||||
"Password for signing",
|
|
||||||
fmt.Sprintf("Please enter password for signing data with account %s", account.Address.Hex()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Sign the data with the wallet
|
|
||||||
signature, err := wallet.SignDataWithPassphrase(account, pw, req.ContentType, req.Rawdata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if legacyV {
|
|
||||||
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
|
||||||
}
|
|
||||||
return signature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignData signs the hash of the provided data, but does so differently
|
|
||||||
// depending on the content-type specified.
|
|
||||||
//
|
|
||||||
// Different types of validation occur.
|
|
||||||
func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
|
|
||||||
var req, transformV, err = api.determineSignatureFormat(ctx, contentType, addr, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
signature, err := api.sign(req, transformV)
|
|
||||||
if err != nil {
|
|
||||||
api.UI.ShowError(err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return signature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// determineSignatureFormat determines which signature method should be used based upon the mime type
|
|
||||||
// In the cases where it matters ensure that the charset is handled. The charset
|
|
||||||
// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType
|
|
||||||
// charset, ok := params["charset"]
|
|
||||||
// As it is now, we accept any charset and just treat it as 'raw'.
|
|
||||||
// This method returns the mimetype for signing along with the request
|
|
||||||
func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, bool, error) {
|
|
||||||
var (
|
|
||||||
req *SignDataRequest
|
|
||||||
useEthereumV = true // Default to use V = 27 or 28, the legacy Ethereum format
|
|
||||||
)
|
|
||||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mediaType {
|
|
||||||
case apitypes.IntendedValidator.Mime:
|
|
||||||
// Data with an intended validator
|
|
||||||
validatorData, err := UnmarshalValidatorData(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
sighash, msg := SignTextValidator(validatorData)
|
|
||||||
messages := []*apitypes.NameValueType{
|
|
||||||
{
|
|
||||||
Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)",
|
|
||||||
Typ: "description",
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Intended validator address",
|
|
||||||
Typ: "address",
|
|
||||||
Value: validatorData.Address.String(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Application-specific data",
|
|
||||||
Typ: "hexdata",
|
|
||||||
Value: validatorData.Message,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Full message for signing",
|
|
||||||
Typ: "hexdata",
|
|
||||||
Value: fmt.Sprintf("%#x", msg),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
|
|
||||||
case apitypes.ApplicationClique.Mime:
|
|
||||||
// Clique is the Ethereum PoA standard
|
|
||||||
cliqueData, err := fromHex(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
header := &types.Header{}
|
|
||||||
if err := rlp.DecodeBytes(cliqueData, header); err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
// Add space in the extradata to put the signature
|
|
||||||
newExtra := make([]byte, len(header.Extra)+65)
|
|
||||||
copy(newExtra, header.Extra)
|
|
||||||
header.Extra = newExtra
|
|
||||||
|
|
||||||
// Get back the rlp data, encoded by us
|
|
||||||
sighash, cliqueRlp, err := cliqueHeaderHashAndRlp(header)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
messages := []*apitypes.NameValueType{
|
|
||||||
{
|
|
||||||
Name: "Clique header",
|
|
||||||
Typ: "clique",
|
|
||||||
Value: fmt.Sprintf("clique header %d [%#x]", header.Number, header.Hash()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Clique uses V on the form 0 or 1
|
|
||||||
useEthereumV = false
|
|
||||||
req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash}
|
|
||||||
case apitypes.DataTyped.Mime:
|
|
||||||
// EIP-712 conformant typed data
|
|
||||||
var err error
|
|
||||||
req, err = typedDataRequest(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
default: // also case TextPlain.Mime:
|
|
||||||
// Calculates an Ethereum ECDSA signature for:
|
|
||||||
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
|
|
||||||
// We expect input to be a hex-encoded string
|
|
||||||
textData, err := fromHex(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, useEthereumV, err
|
|
||||||
}
|
|
||||||
sighash, msg := accounts.TextAndHash(textData)
|
|
||||||
messages := []*apitypes.NameValueType{
|
|
||||||
{
|
|
||||||
Name: "message",
|
|
||||||
Typ: accounts.MimetypeTextPlain,
|
|
||||||
Value: msg,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
|
|
||||||
}
|
|
||||||
req.Address = addr
|
|
||||||
req.Meta = MetadataFromContext(ctx)
|
|
||||||
return req, useEthereumV, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignTextValidator signs the given message which can be further recovered
|
|
||||||
// with the given validator.
|
|
||||||
// hash = keccak256("\x19\x00"${address}${data}).
|
|
||||||
func SignTextValidator(validatorData apitypes.ValidatorData) (hexutil.Bytes, string) {
|
|
||||||
msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message))
|
|
||||||
return crypto.Keccak256([]byte(msg)), msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// cliqueHeaderHashAndRlp returns the hash which is used as input for the proof-of-authority
|
|
||||||
// signing. It is the hash of the entire header apart from the 65 byte signature
|
|
||||||
// contained at the end of the extra data.
|
|
||||||
//
|
|
||||||
// The method requires the extra data to be at least 65 bytes -- the original implementation
|
|
||||||
// in clique.go panics if this is the case, thus it's been reimplemented here to avoid the panic
|
|
||||||
// and simply return an error instead
|
|
||||||
func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) {
|
|
||||||
if len(header.Extra) < 65 {
|
|
||||||
err = fmt.Errorf("clique header extradata too short, %d < 65", len(header.Extra))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rlp = clique.CliqueRLP(header)
|
|
||||||
hash = clique.SealHash(header).Bytes()
|
|
||||||
return hash, rlp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignTypedData signs EIP-712 conformant typed data
|
|
||||||
// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
|
|
||||||
// It returns
|
|
||||||
// - the signature,
|
|
||||||
// - and/or any error
|
|
||||||
func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData apitypes.TypedData) (hexutil.Bytes, error) {
|
|
||||||
signature, _, err := api.signTypedData(ctx, addr, typedData, nil)
|
|
||||||
return signature, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
|
|
||||||
// - the signature preimage (hash)
|
|
||||||
func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress,
|
|
||||||
typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
|
|
||||||
req, err := typedDataRequest(typedData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
req.Address = addr
|
|
||||||
req.Meta = MetadataFromContext(ctx)
|
|
||||||
if validationMessages != nil {
|
|
||||||
req.Callinfo = validationMessages.Messages
|
|
||||||
}
|
|
||||||
signature, err := api.sign(req, true)
|
|
||||||
if err != nil {
|
|
||||||
api.UI.ShowError(err.Error())
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return signature, req.Hash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fromHex tries to interpret the data as type string, and convert from
|
|
||||||
// hexadecimal to []byte
|
|
||||||
func fromHex(data any) ([]byte, error) {
|
|
||||||
if stringData, ok := data.(string); ok {
|
|
||||||
binary, err := hexutil.Decode(stringData)
|
|
||||||
return binary, err
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("wrong type %T", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// typeDataRequest tries to convert the data into a SignDataRequest.
|
|
||||||
func typedDataRequest(data any) (*SignDataRequest, error) {
|
|
||||||
var typedData apitypes.TypedData
|
|
||||||
if td, ok := data.(apitypes.TypedData); ok {
|
|
||||||
typedData = td
|
|
||||||
} else { // Hex-encoded data
|
|
||||||
jsonData, err := fromHex(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(jsonData, &typedData); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
messages, err := typedData.Format()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sighash, rawData, err := apitypes.TypedDataAndHash(typedData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &SignDataRequest{
|
|
||||||
ContentType: apitypes.DataTyped.Mime,
|
|
||||||
Rawdata: []byte(rawData),
|
|
||||||
Messages: messages,
|
|
||||||
Hash: sighash}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EcRecover recovers the address associated with the given sig.
|
|
||||||
// Only compatible with `text/plain`
|
|
||||||
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
|
|
||||||
// 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://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover
|
|
||||||
if len(sig) != 65 {
|
|
||||||
return common.Address{}, errors.New("signature must be 65 bytes long")
|
|
||||||
}
|
|
||||||
if sig[64] != 27 && sig[64] != 28 {
|
|
||||||
return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)")
|
|
||||||
}
|
|
||||||
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
|
||||||
hash := accounts.TextHash(data)
|
|
||||||
rpk, err := crypto.SigToPub(hash, sig)
|
|
||||||
if err != nil {
|
|
||||||
return common.Address{}, err
|
|
||||||
}
|
|
||||||
return crypto.PubkeyToAddress(*rpk), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalValidatorData converts the bytes input to typed data
|
|
||||||
func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) {
|
|
||||||
raw, ok := data.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return apitypes.ValidatorData{}, errors.New("validator input is not a map[string]interface{}")
|
|
||||||
}
|
|
||||||
addrBytes, err := fromHex(raw["address"])
|
|
||||||
if err != nil {
|
|
||||||
return apitypes.ValidatorData{}, fmt.Errorf("validator address error: %w", err)
|
|
||||||
}
|
|
||||||
if len(addrBytes) == 0 {
|
|
||||||
return apitypes.ValidatorData{}, errors.New("validator address is undefined")
|
|
||||||
}
|
|
||||||
messageBytes, err := fromHex(raw["message"])
|
|
||||||
if err != nil {
|
|
||||||
return apitypes.ValidatorData{}, fmt.Errorf("message error: %w", err)
|
|
||||||
}
|
|
||||||
if len(messageBytes) == 0 {
|
|
||||||
return apitypes.ValidatorData{}, errors.New("message is undefined")
|
|
||||||
}
|
|
||||||
return apitypes.ValidatorData{
|
|
||||||
Address: common.BytesToAddress(addrBytes),
|
|
||||||
Message: messageBytes,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,120 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StdIOUI struct {
|
|
||||||
client *rpc.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStdIOUI() *StdIOUI {
|
|
||||||
client, err := rpc.DialContext(context.Background(), "stdio://")
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Could not create stdio client", "err", err)
|
|
||||||
}
|
|
||||||
ui := &StdIOUI{client: client}
|
|
||||||
return ui
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) RegisterUIServer(api *UIServerAPI) {
|
|
||||||
ui.client.RegisterName("clef", api)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatch sends a request over the stdio
|
|
||||||
func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error {
|
|
||||||
err := ui.client.Call(&reply, serviceMethod, args)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error", "exc", err.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// notify sends a request over the stdio, and does not listen for a response
|
|
||||||
func (ui *StdIOUI) notify(serviceMethod string, args interface{}) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
err := ui.client.Notify(ctx, serviceMethod, args)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error", "exc", err.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
|
|
||||||
var result SignTxResponse
|
|
||||||
err := ui.dispatch("ui_approveTx", request, &result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
|
|
||||||
var result SignDataResponse
|
|
||||||
err := ui.dispatch("ui_approveSignData", request, &result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
|
|
||||||
var result ListResponse
|
|
||||||
err := ui.dispatch("ui_approveListing", request, &result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
|
|
||||||
var result NewAccountResponse
|
|
||||||
err := ui.dispatch("ui_approveNewAccount", request, &result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ShowError(message string) {
|
|
||||||
err := ui.notify("ui_showError", &Message{message})
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error calling 'ui_showError'", "exc", err.Error(), "msg", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) ShowInfo(message string) {
|
|
||||||
err := ui.notify("ui_showInfo", Message{message})
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error calling 'ui_showInfo'", "exc", err.Error(), "msg", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
err := ui.notify("ui_onApprovedTx", tx)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error calling 'ui_onApprovedTx'", "exc", err.Error(), "tx", tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
|
|
||||||
err := ui.notify("ui_onSignerStartup", info)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error calling 'ui_onSignerStartup'", "exc", err.Error(), "info", info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
|
||||||
var result UserInputResponse
|
|
||||||
err := ui.dispatch("ui_onInputRequired", info, &result)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Error calling 'ui_onInputRequired'", "exc", err.Error(), "info", info)
|
|
||||||
}
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
5
signer/core/testdata/README.md
vendored
5
signer/core/testdata/README.md
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
### EIP 712 tests
|
|
||||||
|
|
||||||
These tests are json files which are converted into eip-712 typed data.
|
|
||||||
All files are expected to be proper json, and tests will fail if they are not.
|
|
||||||
Files that begin with `expfail' are expected to not pass the hashstruct construction.
|
|
||||||
60
signer/core/testdata/arrays-1.json
vendored
60
signer/core/testdata/arrays-1.json
vendored
|
|
@ -1,60 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Foo": [
|
|
||||||
{
|
|
||||||
"name": "addys",
|
|
||||||
"type": "address[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stringies",
|
|
||||||
"type": "string[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "inties",
|
|
||||||
"type": "uint[]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Foo",
|
|
||||||
"domain": {
|
|
||||||
"name": "Lorem",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"addys": [
|
|
||||||
"0x0000000000000000000000000000000000000001",
|
|
||||||
"0x0000000000000000000000000000000000000002",
|
|
||||||
"0x0000000000000000000000000000000000000003"
|
|
||||||
],
|
|
||||||
"stringies": [
|
|
||||||
"lorem",
|
|
||||||
"ipsum",
|
|
||||||
"dolores"
|
|
||||||
],
|
|
||||||
"inties": [
|
|
||||||
"0x0000000000000000000000000000000000000001",
|
|
||||||
"3",
|
|
||||||
4.0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
signer/core/testdata/custom_arraytype.json
vendored
54
signer/core/testdata/custom_arraytype.json
vendored
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": { "name": "Cow"},
|
|
||||||
"to": [{ "name": "Moose"},{ "name": "Goose"}],
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
76
signer/core/testdata/eip712.json
vendored
76
signer/core/testdata/eip712.json
vendored
|
|
@ -1,76 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test2",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"test": "3",
|
|
||||||
"test2": 5.0,
|
|
||||||
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"test": "0",
|
|
||||||
"test2": 5,
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person[]": [
|
|
||||||
{
|
|
||||||
"name": "baz",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {"baz": "foo"},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
signer/core/testdata/expfail_datamismatch_1.json
vendored
64
signer/core/testdata/expfail_datamismatch_1.json
vendored
|
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "Person"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
77
signer/core/testdata/expfail_extradata.json
vendored
77
signer/core/testdata/expfail_extradata.json
vendored
|
|
@ -1,77 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test2",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"blahonga": "zonk bonk",
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"test": "3",
|
|
||||||
"test2": 5.0,
|
|
||||||
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"test": "0",
|
|
||||||
"test2": 5,
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"vFAILFAILerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "Blahonga"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256 ... and now for something completely different"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test2",
|
|
||||||
"type": "uint8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"test": "3",
|
|
||||||
"test2": 5.0,
|
|
||||||
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "Bob",
|
|
||||||
"test": "0",
|
|
||||||
"test2": 5,
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
signer/core/testdata/expfail_toolargeuint.json
vendored
38
signer/core/testdata/expfail_toolargeuint.json
vendored
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test":"257"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
signer/core/testdata/expfail_toolargeuint2.json
vendored
38
signer/core/testdata/expfail_toolargeuint2.json
vendored
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test":257
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test":"255.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test": 255.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test":"255.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Foo": [
|
|
||||||
{
|
|
||||||
"name": "addys",
|
|
||||||
"type": "address[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stringies",
|
|
||||||
"type": "string[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "inties",
|
|
||||||
"type": "uint[]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Foo",
|
|
||||||
"domain": {
|
|
||||||
"name": "Lorem",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"addys": [
|
|
||||||
"0x0000000000000000000000000000000000000001",
|
|
||||||
"0x0000000000000000000000000000000000000002",
|
|
||||||
"0x0000000000000000000000000000000000000003"
|
|
||||||
],
|
|
||||||
"stringies": [
|
|
||||||
"lorem",
|
|
||||||
"ipsum",
|
|
||||||
"dolores"
|
|
||||||
],
|
|
||||||
"inties": [
|
|
||||||
"0x0000000000000000000000000000000000000001",
|
|
||||||
"3",
|
|
||||||
4.0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "test",
|
|
||||||
"type": "uint8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "Ether Mail",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"test":257
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"domain":{"version":"0","chainId":""}}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
{ "types": { "":[ {
|
|
||||||
"name": "name",
|
|
||||||
"type":"string" },
|
|
||||||
{
|
|
||||||
"name":"version",
|
|
||||||
"type": "string" }, {
|
|
||||||
"name": "chaiI",
|
|
||||||
"type":"uint256 . ad nowretig omeedifere" }, {
|
|
||||||
"ae": "eifinC",
|
|
||||||
"ty":"dess"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person":[
|
|
||||||
{
|
|
||||||
"name":"name",
|
|
||||||
"type": "string"
|
|
||||||
}, {
|
|
||||||
"name":"tes", "type":"it8"
|
|
||||||
},
|
|
||||||
{ "name":"t", "tye":"uit8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a":"ale",
|
|
||||||
"type": "ress"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name":"from", "type":"Person" },
|
|
||||||
{
|
|
||||||
"name": "to", "type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contents",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, "primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name":"theMail", "version": "1",
|
|
||||||
"chainId": "1",
|
|
||||||
"verifyingntract": "0xCcccCCCcCCCCCCCcCCcCCCcCcccccC"
|
|
||||||
},
|
|
||||||
"message": { "from": {
|
|
||||||
"name": "Cow",
|
|
||||||
"test": "3",
|
|
||||||
"est2":5.0,
|
|
||||||
"llt": "0xcD2a3938E13D947E0bE734DfDD86" }, "to": { "name": "Bob",
|
|
||||||
"ts":"",
|
|
||||||
"tet2": 5,
|
|
||||||
"allet": "0bBBBBbbBBbbbbBbbBbbbbBBBbB"
|
|
||||||
},
|
|
||||||
"contents": "Hello, Bob!" }
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"EIP712Domain": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "chainId",
|
|
||||||
"type": "int"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verifyingContract",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Person": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wallet",
|
|
||||||
"type": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Mail": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "Mail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "s",
|
|
||||||
"type": "Person"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"primaryType": "Mail",
|
|
||||||
"domain": {
|
|
||||||
"name": "l",
|
|
||||||
"version": "1",
|
|
||||||
"chainId": "",
|
|
||||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"from": {
|
|
||||||
"name": "",
|
|
||||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"name": "",
|
|
||||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
|
||||||
},
|
|
||||||
"": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"types":{"0":[{}]}}
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
// Copyright 2019 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UIServerAPI implements methods Clef provides for a UI to query, in the bidirectional communication
|
|
||||||
// channel.
|
|
||||||
// This API is considered secure, since a request can only
|
|
||||||
// ever arrive from the UI -- and the UI is capable of approving any action, thus we can consider these
|
|
||||||
// requests pre-approved.
|
|
||||||
// NB: It's very important that these methods are not ever exposed on the external service
|
|
||||||
// registry.
|
|
||||||
type UIServerAPI struct {
|
|
||||||
extApi *SignerAPI
|
|
||||||
am *accounts.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUIServerAPI creates a new UIServerAPI
|
|
||||||
func NewUIServerAPI(extapi *SignerAPI) *UIServerAPI {
|
|
||||||
return &UIServerAPI{extapi, extapi.am}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List available accounts. As opposed to the external API definition, this method delivers
|
|
||||||
// the full Account object and not only Address.
|
|
||||||
// Example call
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_listAccounts","params":[], "id":4}
|
|
||||||
func (s *UIServerAPI) ListAccounts(ctx context.Context) ([]accounts.Account, error) {
|
|
||||||
var accs []accounts.Account
|
|
||||||
for _, wallet := range s.am.Wallets() {
|
|
||||||
accs = append(accs, wallet.Accounts()...)
|
|
||||||
}
|
|
||||||
return accs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 that clef manages
|
|
||||||
// Example call
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_listWallets","params":[], "id":5}
|
|
||||||
func (s *UIServerAPI) ListWallets() []rawWallet {
|
|
||||||
wallets := make([]rawWallet, 0) // return [] instead of nil if empty
|
|
||||||
for _, wallet := range s.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
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveAccount requests a HD wallet to derive a new account, optionally pinning
|
|
||||||
// it for later reuse.
|
|
||||||
// Example call
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_deriveAccount","params":["ledger://","m/44'/60'/0'", false], "id":6}
|
|
||||||
func (s *UIServerAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) {
|
|
||||||
wallet, err := s.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchKeystore retrieves the encrypted keystore from the account manager.
|
|
||||||
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
|
|
||||||
ks := am.Backends(keystore.KeyStoreType)
|
|
||||||
if len(ks) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ks[0].(*keystore.KeyStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
|
||||||
// encrypting it with the passphrase.
|
|
||||||
// Example call (should fail on password too short)
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_importRawKey","params":["1111111111111111111111111111111111111111111111111111111111111111","test"], "id":6}
|
|
||||||
func (s *UIServerAPI) ImportRawKey(privkey string, password string) (accounts.Account, error) {
|
|
||||||
key, err := crypto.HexToECDSA(privkey)
|
|
||||||
if err != nil {
|
|
||||||
return accounts.Account{}, err
|
|
||||||
}
|
|
||||||
if err := ValidatePasswordFormat(password); err != nil {
|
|
||||||
return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err)
|
|
||||||
}
|
|
||||||
// No error
|
|
||||||
return fetchKeystore(s.am).ImportECDSA(key, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
// Example
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_openWallet","params":["ledger://",""], "id":6}
|
|
||||||
func (s *UIServerAPI) OpenWallet(url string, passphrase *string) error {
|
|
||||||
wallet, err := s.am.Wallet(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pass := ""
|
|
||||||
if passphrase != nil {
|
|
||||||
pass = *passphrase
|
|
||||||
}
|
|
||||||
return wallet.Open(pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainId returns the chainid in use for Eip-155 replay protection
|
|
||||||
// Example call
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_chainId","params":[], "id":8}
|
|
||||||
func (s *UIServerAPI) ChainId() math.HexOrDecimal64 {
|
|
||||||
return (math.HexOrDecimal64)(s.extApi.chainID.Uint64())
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChainId sets the chain id to use when signing transactions.
|
|
||||||
// Example call to set Ropsten:
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_setChainId","params":["3"], "id":8}
|
|
||||||
func (s *UIServerAPI) SetChainId(id math.HexOrDecimal64) math.HexOrDecimal64 {
|
|
||||||
s.extApi.chainID = new(big.Int).SetUint64(uint64(id))
|
|
||||||
return s.ChainId()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export returns encrypted private key associated with the given address in web3 keystore format.
|
|
||||||
// Example
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_export","params":["0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"], "id":4}
|
|
||||||
func (s *UIServerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
|
|
||||||
// Look up the wallet containing the requested signer
|
|
||||||
wallet, err := s.am.Find(accounts.Account{Address: addr})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if wallet.URL().Scheme != keystore.KeyStoreScheme {
|
|
||||||
return nil, errors.New("account is not a keystore-account")
|
|
||||||
}
|
|
||||||
return os.ReadFile(wallet.URL().Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
|
|
||||||
// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
|
|
||||||
// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
|
|
||||||
// Example (the address in question has privkey `11...11`):
|
|
||||||
// {"jsonrpc":"2.0","method":"clef_import","params":[{"address":"19e7e376e7c213b7e7e7e46cc70a5dd086daff2a","crypto":{"cipher":"aes-128-ctr","ciphertext":"33e4cd3756091d037862bb7295e9552424a391a6e003272180a455ca2a9fb332","cipherparams":{"iv":"b54b263e8f89c42bb219b6279fba5cce"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"e4ca94644fd30569c1b1afbbc851729953c92637b7fe4bb9840bbb31ffbc64a5"},"mac":"f4092a445c2b21c0ef34f17c9cd0d873702b2869ec5df4439a0c2505823217e7"},"id":"216c7eac-e8c1-49af-a215-fa0036f29141","version":3},"test","yaddayadda"], "id":4}
|
|
||||||
func (api *UIServerAPI) Import(ctx context.Context, keyJSON json.RawMessage, oldPassphrase, newPassphrase string) (accounts.Account, error) {
|
|
||||||
be := api.am.Backends(keystore.KeyStoreType)
|
|
||||||
|
|
||||||
if len(be) == 0 {
|
|
||||||
return accounts.Account{}, errors.New("password based accounts not supported")
|
|
||||||
}
|
|
||||||
if err := ValidatePasswordFormat(newPassphrase); err != nil {
|
|
||||||
return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err)
|
|
||||||
}
|
|
||||||
return be[0].(*keystore.KeyStore).Import(keyJSON, oldPassphrase, newPassphrase)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new password protected Account. The private key is protected with
|
|
||||||
// the given password. Users are responsible to backup the private key that is stored
|
|
||||||
// in the keystore location that was specified when this API was created.
|
|
||||||
// This method is the same as New on the external API, the difference being that
|
|
||||||
// this implementation does not ask for confirmation, since it's initiated by
|
|
||||||
// the user
|
|
||||||
func (api *UIServerAPI) New(ctx context.Context) (common.Address, error) {
|
|
||||||
return api.extApi.newAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other methods to be added, not yet implemented are:
|
|
||||||
// - Ruleset interaction: add rules, attest rulefiles
|
|
||||||
// - Store metadata about accounts, e.g. naming of accounts
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var printable7BitAscii = regexp.MustCompile("^[A-Za-z0-9!\"#$%&'()*+,\\-./:;<=>?@[\\]^_`{|}~ ]+$")
|
|
||||||
|
|
||||||
// ValidatePasswordFormat returns an error if the password is too short, or consists of characters
|
|
||||||
// outside the range of the printable 7bit ascii set
|
|
||||||
func ValidatePasswordFormat(password string) error {
|
|
||||||
if len(password) < 10 {
|
|
||||||
return errors.New("password too short (<10 characters)")
|
|
||||||
}
|
|
||||||
if !printable7BitAscii.MatchString(password) {
|
|
||||||
return errors.New("password contains invalid characters - only 7bit printable ascii allowed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
// Copyright 2018 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 core
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestPasswordValidation(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testcases := []struct {
|
|
||||||
pw string
|
|
||||||
shouldFail bool
|
|
||||||
}{
|
|
||||||
{"test", true},
|
|
||||||
{"testtest\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98", true},
|
|
||||||
{"placeOfInterest⌘", true},
|
|
||||||
{"password\nwith\nlinebreak", true},
|
|
||||||
{"password\twith\vtabs", true},
|
|
||||||
// Ok passwords
|
|
||||||
{"password WhichIsOk", false},
|
|
||||||
{"passwordOk!@#$%^&*()", false},
|
|
||||||
{"12301203123012301230123012", false},
|
|
||||||
}
|
|
||||||
for _, test := range testcases {
|
|
||||||
err := ValidatePasswordFormat(test.pw)
|
|
||||||
if err == nil && test.shouldFail {
|
|
||||||
t.Errorf("password '%v' should fail validation", test.pw)
|
|
||||||
} else if err != nil && !test.shouldFail {
|
|
||||||
t.Errorf("password '%v' shound not fail validation, but did: %v", test.pw, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
268623
signer/fourbyte/4byte.json
268623
signer/fourbyte/4byte.json
File diff suppressed because it is too large
Load diff
|
|
@ -1,136 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// decodedCallData is an internal type to represent a method call parsed according
|
|
||||||
// to an ABI method signature.
|
|
||||||
type decodedCallData struct {
|
|
||||||
signature string
|
|
||||||
name string
|
|
||||||
inputs []decodedArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodedArgument is an internal type to represent an argument parsed according
|
|
||||||
// to an ABI method signature.
|
|
||||||
type decodedArgument struct {
|
|
||||||
soltype abi.Argument
|
|
||||||
value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements stringer interface, tries to use the underlying value-type
|
|
||||||
func (arg decodedArgument) String() string {
|
|
||||||
var value string
|
|
||||||
switch val := arg.value.(type) {
|
|
||||||
case fmt.Stringer:
|
|
||||||
value = val.String()
|
|
||||||
default:
|
|
||||||
value = fmt.Sprintf("%v", val)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%v: %v", arg.soltype.Type.String(), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements stringer interface for decodedCallData
|
|
||||||
func (cd decodedCallData) String() string {
|
|
||||||
args := make([]string, len(cd.inputs))
|
|
||||||
for i, arg := range cd.inputs {
|
|
||||||
args[i] = arg.String()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s(%s)", cd.name, strings.Join(args, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifySelector checks whether the ABI encoded data blob matches the requested
|
|
||||||
// function signature.
|
|
||||||
func verifySelector(selector string, calldata []byte) (*decodedCallData, error) {
|
|
||||||
// Parse the selector into an ABI JSON spec
|
|
||||||
abidata, err := parseSelector(selector)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Parse the call data according to the requested selector
|
|
||||||
return parseCallData(calldata, string(abidata))
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSelector converts a method selector into an ABI JSON spec. The returned
|
|
||||||
// data is a valid JSON string which can be consumed by the standard abi package.
|
|
||||||
func parseSelector(unescapedSelector string) ([]byte, error) {
|
|
||||||
selector, err := abi.ParseSelector(unescapedSelector)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse selector: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal([]abi.SelectorMarshaling{selector})
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCallData matches the provided call data against the ABI definition and
|
|
||||||
// returns a struct containing the actual go-typed values.
|
|
||||||
func parseCallData(calldata []byte, unescapedAbidata string) (*decodedCallData, error) {
|
|
||||||
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
|
|
||||||
if len(calldata) < 4 {
|
|
||||||
return nil, fmt.Errorf("invalid call data, incomplete method signature (%d bytes < 4)", len(calldata))
|
|
||||||
}
|
|
||||||
sigdata := calldata[:4]
|
|
||||||
|
|
||||||
argdata := calldata[4:]
|
|
||||||
if len(argdata)%32 != 0 {
|
|
||||||
return nil, fmt.Errorf("invalid call data; length should be a multiple of 32 bytes (was %d)", len(argdata))
|
|
||||||
}
|
|
||||||
// Validate the called method and upack the call data accordingly
|
|
||||||
abispec, err := abi.JSON(strings.NewReader(unescapedAbidata))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid method signature (%q): %v", unescapedAbidata, err)
|
|
||||||
}
|
|
||||||
method, err := abispec.MethodById(sigdata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
values, err := method.Inputs.UnpackValues(argdata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("signature %q matches, but arguments mismatch: %v", method.String(), err)
|
|
||||||
}
|
|
||||||
// Everything valid, assemble the call infos for the signer
|
|
||||||
decoded := decodedCallData{signature: method.Sig, name: method.RawName}
|
|
||||||
for i := 0; i < len(method.Inputs); i++ {
|
|
||||||
decoded.inputs = append(decoded.inputs, decodedArgument{
|
|
||||||
soltype: method.Inputs[i],
|
|
||||||
value: values[i],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// We're finished decoding the data. At this point, we encode the decoded data
|
|
||||||
// to see if it matches with the original data. If we didn't do that, it would
|
|
||||||
// be possible to stuff extra data into the arguments, which is not detected
|
|
||||||
// by merely decoding the data.
|
|
||||||
encoded, err := method.Inputs.PackValues(values)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(encoded, argdata) {
|
|
||||||
was := common.Bytes2Hex(encoded)
|
|
||||||
exp := common.Bytes2Hex(argdata)
|
|
||||||
return nil, fmt.Errorf("WARNING: Supplied data is stuffed with extra data. \nWant %s\nHave %s\nfor method %v", exp, was, method.Sig)
|
|
||||||
}
|
|
||||||
return &decoded, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
|
|
||||||
abispec, err := abi.JSON(strings.NewReader(jsondata))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cd := common.Hex2Bytes(calldata)
|
|
||||||
sigdata, argdata := cd[:4], cd[4:]
|
|
||||||
method, err := abispec.MethodById(sigdata)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
data, err := method.Inputs.UnpackValues(argdata)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(data) != len(exp) {
|
|
||||||
t.Fatalf("Mismatched length, expected %d, got %d", len(exp), len(data))
|
|
||||||
}
|
|
||||||
for i, elem := range data {
|
|
||||||
if !reflect.DeepEqual(elem, exp[i]) {
|
|
||||||
t.Fatalf("Unpack error, arg %d, got %v, want %v", i, elem, exp[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewUnpacker(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
type unpackTest struct {
|
|
||||||
jsondata string
|
|
||||||
calldata string
|
|
||||||
exp []interface{}
|
|
||||||
}
|
|
||||||
testcases := []unpackTest{
|
|
||||||
{ // https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
|
|
||||||
`[{"type":"function","name":"f", "inputs":[{"type":"uint256"},{"type":"uint32[]"},{"type":"bytes10"},{"type":"bytes"}]}]`,
|
|
||||||
// 0x123, [0x456, 0x789], "1234567890", "Hello, world!"
|
|
||||||
"8be65246" + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000",
|
|
||||||
[]interface{}{
|
|
||||||
big.NewInt(0x123),
|
|
||||||
[]uint32{0x456, 0x789},
|
|
||||||
[10]byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48},
|
|
||||||
common.Hex2Bytes("48656c6c6f2c20776f726c6421"),
|
|
||||||
},
|
|
||||||
}, { // https://docs.soliditylang.org/en/develop/abi-spec.html#examples
|
|
||||||
`[{"type":"function","name":"sam","inputs":[{"type":"bytes"},{"type":"bool"},{"type":"uint256[]"}]}]`,
|
|
||||||
// "dave", true and [1,2,3]
|
|
||||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
|
|
||||||
[]interface{}{
|
|
||||||
[]byte{0x64, 0x61, 0x76, 0x65},
|
|
||||||
true,
|
|
||||||
[]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
`[{"type":"function","name":"send","inputs":[{"type":"uint256"}]}]`,
|
|
||||||
"a52c101e0000000000000000000000000000000000000000000000000000000000000012",
|
|
||||||
[]interface{}{big.NewInt(0x12)},
|
|
||||||
}, {
|
|
||||||
`[{"type":"function","name":"compareAndApprove","inputs":[{"type":"address"},{"type":"uint256"},{"type":"uint256"}]}]`,
|
|
||||||
"751e107900000000000000000000000000000133700000deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
[]interface{}{
|
|
||||||
common.HexToAddress("0x00000133700000deadbeef000000000000000000"),
|
|
||||||
new(big.Int).SetBytes([]byte{0x00}),
|
|
||||||
big.NewInt(0x1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, c := range testcases {
|
|
||||||
verify(t, c.jsondata, c.calldata, c.exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalldataDecoding(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// send(uint256) : a52c101e
|
|
||||||
// compareAndApprove(address,uint256,uint256) : 751e1079
|
|
||||||
// issue(address[],uint256) : 42958b54
|
|
||||||
jsondata := `
|
|
||||||
[
|
|
||||||
{"type":"function","name":"send","inputs":[{"name":"a","type":"uint256"}]},
|
|
||||||
{"type":"function","name":"compareAndApprove","inputs":[{"name":"a","type":"address"},{"name":"a","type":"uint256"},{"name":"a","type":"uint256"}]},
|
|
||||||
{"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"name":"a","type":"uint256"}]},
|
|
||||||
{"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]}
|
|
||||||
]`
|
|
||||||
// Expected failures
|
|
||||||
for i, hexdata := range []string{
|
|
||||||
"a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
|
||||||
"a52c101e000000000000000000000000000000000000000000000000000000000000001200",
|
|
||||||
"a52c101e00000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"a52c101e",
|
|
||||||
"a52c10",
|
|
||||||
"",
|
|
||||||
// Too short
|
|
||||||
"751e10790000000000000000000000000000000000000000000000000000000000000012",
|
|
||||||
"751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
||||||
// Not valid multiple of 32
|
|
||||||
"deadbeef00000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
// Too short 'issue'
|
|
||||||
"42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
|
||||||
// Too short compareAndApprove
|
|
||||||
"a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
|
|
||||||
// From https://docs.soliditylang.org/en/develop/abi-spec.html
|
|
||||||
// contains a bool with illegal values
|
|
||||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
|
|
||||||
} {
|
|
||||||
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d: expected decoding to fail: %s", i, hexdata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Expected success
|
|
||||||
for i, hexdata := range []string{
|
|
||||||
// From https://docs.soliditylang.org/en/develop/abi-spec.html
|
|
||||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
|
|
||||||
"a52c101e0000000000000000000000000000000000000000000000000000000000000012",
|
|
||||||
"a52c101eFFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
||||||
"751e1079000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"42958b54" +
|
|
||||||
// start of dynamic type
|
|
||||||
"0000000000000000000000000000000000000000000000000000000000000040" +
|
|
||||||
// uint256
|
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001" +
|
|
||||||
// length of array
|
|
||||||
"0000000000000000000000000000000000000000000000000000000000000002" +
|
|
||||||
// array values
|
|
||||||
"000000000000000000000000000000000000000000000000000000000000dead" +
|
|
||||||
"000000000000000000000000000000000000000000000000000000000000beef",
|
|
||||||
} {
|
|
||||||
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: unexpected failure on input %s:\n %v (%d bytes) ", i, hexdata, err, len(common.Hex2Bytes(hexdata)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaliciousABIStrings(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []string{
|
|
||||||
"func(uint256,uint256,[]uint256)",
|
|
||||||
"func(uint256,uint256,uint256,)",
|
|
||||||
"func(,uint256,uint256,uint256)",
|
|
||||||
}
|
|
||||||
data := common.Hex2Bytes("4401a6e40000000000000000000000000000000000000000000000000000000000000012")
|
|
||||||
for i, tt := range tests {
|
|
||||||
_, err := verifySelector(tt, data)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %d: expected error for selector '%v'", i, tt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte contains the 4byte database.
|
|
||||||
package fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed 4byte.json
|
|
||||||
var embeddedJSON []byte
|
|
||||||
|
|
||||||
// Database is a 4byte database with the possibility of maintaining an immutable
|
|
||||||
// set (embedded) into the process and a mutable set (loaded and written to file).
|
|
||||||
type Database struct {
|
|
||||||
embedded map[string]string
|
|
||||||
custom map[string]string
|
|
||||||
customPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// newEmpty exists for testing purposes.
|
|
||||||
func newEmpty() *Database {
|
|
||||||
return &Database{
|
|
||||||
embedded: make(map[string]string),
|
|
||||||
custom: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New loads the standard signature database embedded in the package.
|
|
||||||
func New() (*Database, error) {
|
|
||||||
return NewWithFile("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFromFile loads signature database from file, and errors if the file is not
|
|
||||||
// valid JSON. The constructor does no other validation of contents. This method
|
|
||||||
// does not load the embedded 4byte database.
|
|
||||||
//
|
|
||||||
// The provided path will be used to write new values into if they are submitted
|
|
||||||
// via the API.
|
|
||||||
func NewFromFile(path string) (*Database, error) {
|
|
||||||
raw, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer raw.Close()
|
|
||||||
|
|
||||||
db := newEmpty()
|
|
||||||
if err := json.NewDecoder(raw).Decode(&db.embedded); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithFile loads both the standard signature database (embedded resource
|
|
||||||
// file) as well as a custom database. The latter will be used to write new
|
|
||||||
// values into if they are submitted via the API.
|
|
||||||
func NewWithFile(path string) (*Database, error) {
|
|
||||||
db := &Database{make(map[string]string), make(map[string]string), path}
|
|
||||||
db.customPath = path
|
|
||||||
|
|
||||||
if err := json.Unmarshal(embeddedJSON, &db.embedded); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Custom file may not exist. Will be created during save, if needed.
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
var blob []byte
|
|
||||||
if blob, err = os.ReadFile(path); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(blob, &db.custom); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the number of 4byte entries in the embedded and custom datasets.
|
|
||||||
func (db *Database) Size() (int, int) {
|
|
||||||
return len(db.embedded), len(db.custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selector checks the given 4byte ID against the known ABI methods.
|
|
||||||
//
|
|
||||||
// This method does not validate the match, it's assumed the caller will do.
|
|
||||||
func (db *Database) Selector(id []byte) (string, error) {
|
|
||||||
if len(id) < 4 {
|
|
||||||
return "", fmt.Errorf("expected 4-byte id, got %d", len(id))
|
|
||||||
}
|
|
||||||
sig := hex.EncodeToString(id[:4])
|
|
||||||
if selector, exists := db.embedded[sig]; exists {
|
|
||||||
return selector, nil
|
|
||||||
}
|
|
||||||
if selector, exists := db.custom[sig]; exists {
|
|
||||||
return selector, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("signature %v not found", sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSelector inserts a new 4byte entry into the database. If custom database
|
|
||||||
// saving is enabled, the new dataset is also persisted to disk.
|
|
||||||
//
|
|
||||||
// Node, this method does _not_ validate the correctness of the data. It assumes
|
|
||||||
// the caller has already done so.
|
|
||||||
func (db *Database) AddSelector(selector string, data []byte) error {
|
|
||||||
// If the selector is already known, skip duplicating it
|
|
||||||
if len(data) < 4 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := db.Selector(data[:4]); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Inject the custom selector into the database and persist if needed
|
|
||||||
db.custom[hex.EncodeToString(data[:4])] = selector
|
|
||||||
if db.customPath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
blob, err := json.Marshal(db.custom)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(db.customPath, blob, 0600)
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that all the selectors contained in the 4byte database are valid.
|
|
||||||
func TestEmbeddedDatabase(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
db, err := New()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var abistruct abi.ABI
|
|
||||||
for id, selector := range db.embedded {
|
|
||||||
abistring, err := parseSelector(selector)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to convert selector to ABI: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(abistring, &abistruct); err != nil {
|
|
||||||
t.Errorf("Failed to parse ABI: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m, err := abistruct.MethodById(common.Hex2Bytes(id))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get method by id (%s): %v", id, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if m.Sig != selector {
|
|
||||||
t.Errorf("Selector mismatch: have %v, want %v", m.Sig, selector)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that custom 4byte datasets can be handled too.
|
|
||||||
func TestCustomDatabase(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// Create a new custom 4byte database with no embedded component
|
|
||||||
tmpdir := t.TempDir()
|
|
||||||
filename := fmt.Sprintf("%s/4byte_custom.json", tmpdir)
|
|
||||||
|
|
||||||
db, err := NewWithFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
db.embedded = make(map[string]string)
|
|
||||||
|
|
||||||
// Ensure the database is empty, insert and verify
|
|
||||||
calldata := common.Hex2Bytes("a52c101edeadbeef")
|
|
||||||
if _, err = db.Selector(calldata); err == nil {
|
|
||||||
t.Fatalf("Should not find a match on empty database")
|
|
||||||
}
|
|
||||||
if err = db.AddSelector("send(uint256)", calldata); err != nil {
|
|
||||||
t.Fatalf("Failed to save file: %v", err)
|
|
||||||
}
|
|
||||||
if _, err = db.Selector(calldata); err != nil {
|
|
||||||
t.Fatalf("Failed to find a match for abi signature: %v", err)
|
|
||||||
}
|
|
||||||
// Check that the file as persisted to disk by creating a new instance
|
|
||||||
db2, err := NewFromFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create new abidb: %v", err)
|
|
||||||
}
|
|
||||||
if _, err = db2.Selector(calldata); err != nil {
|
|
||||||
t.Fatalf("Failed to find a match for persisted abi signature: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateTransaction does a number of checks on the supplied transaction, and
|
|
||||||
// returns either a list of warnings, or an error (indicating that the transaction
|
|
||||||
// should be immediately rejected).
|
|
||||||
func (db *Database) ValidateTransaction(selector *string, tx *apitypes.SendTxArgs) (*apitypes.ValidationMessages, error) {
|
|
||||||
messages := new(apitypes.ValidationMessages)
|
|
||||||
|
|
||||||
// Prevent accidental erroneous usage of both 'input' and 'data' (show stopper)
|
|
||||||
if tx.Data != nil && tx.Input != nil && !bytes.Equal(*tx.Data, *tx.Input) {
|
|
||||||
return nil, errors.New(`ambiguous request: both "data" and "input" are set and are not identical`)
|
|
||||||
}
|
|
||||||
// Place data on 'data', and nil 'input'
|
|
||||||
var data []byte
|
|
||||||
if tx.Input != nil {
|
|
||||||
tx.Data = tx.Input
|
|
||||||
tx.Input = nil
|
|
||||||
}
|
|
||||||
if tx.Data != nil {
|
|
||||||
data = *tx.Data
|
|
||||||
}
|
|
||||||
// Contract creation doesn't validate call data, handle first
|
|
||||||
if tx.To == nil {
|
|
||||||
// Contract creation should contain sufficient data to deploy a contract. A
|
|
||||||
// typical error is omitting sender due to some quirk in the javascript call
|
|
||||||
// e.g. https://github.com/ethereum/go-ethereum/issues/16106.
|
|
||||||
if len(data) == 0 {
|
|
||||||
// Prevent sending ether into black hole (show stopper)
|
|
||||||
if tx.Value.ToInt().Cmp(big.NewInt(0)) > 0 {
|
|
||||||
return nil, errors.New("transaction will create a contract with value but empty code")
|
|
||||||
}
|
|
||||||
// No value submitted at least, critically Warn, but don't blow up
|
|
||||||
messages.Crit("Transaction will create a contract with empty code")
|
|
||||||
} else if len(data) < 40 { // arbitrary heuristic limit
|
|
||||||
messages.Warn(fmt.Sprintf("Transaction will create a contract, but the payload is suspiciously small (%d bytes)", len(data)))
|
|
||||||
}
|
|
||||||
// Method selector should be nil for contract creation
|
|
||||||
if selector != nil {
|
|
||||||
messages.Warn("Transaction will create a contract, but method selector supplied, indicating an intent to call a method")
|
|
||||||
}
|
|
||||||
return messages, nil
|
|
||||||
}
|
|
||||||
// Not a contract creation, validate as a plain transaction
|
|
||||||
if !tx.To.ValidChecksum() {
|
|
||||||
messages.Warn("Invalid checksum on recipient address")
|
|
||||||
}
|
|
||||||
if bytes.Equal(tx.To.Address().Bytes(), common.Address{}.Bytes()) {
|
|
||||||
messages.Crit("Transaction recipient is the zero address")
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case tx.GasPrice == nil && tx.MaxFeePerGas == nil:
|
|
||||||
messages.Crit("Neither 'gasPrice' nor 'maxFeePerGas' specified.")
|
|
||||||
case tx.GasPrice == nil && tx.MaxPriorityFeePerGas == nil:
|
|
||||||
messages.Crit("Neither 'gasPrice' nor 'maxPriorityFeePerGas' specified.")
|
|
||||||
case tx.GasPrice != nil && tx.MaxFeePerGas != nil:
|
|
||||||
messages.Crit("Both 'gasPrice' and 'maxFeePerGas' specified.")
|
|
||||||
case tx.GasPrice != nil && tx.MaxPriorityFeePerGas != nil:
|
|
||||||
messages.Crit("Both 'gasPrice' and 'maxPriorityFeePerGas' specified.")
|
|
||||||
}
|
|
||||||
// Semantic fields validated, try to make heads or tails of the call data
|
|
||||||
db.ValidateCallData(selector, data, messages)
|
|
||||||
return messages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateCallData checks if the ABI call-data + method selector (if given) can
|
|
||||||
// be parsed and seems to match.
|
|
||||||
func (db *Database) ValidateCallData(selector *string, data []byte, messages *apitypes.ValidationMessages) {
|
|
||||||
// If the data is empty, we have a plain value transfer, nothing more to do
|
|
||||||
if len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
|
|
||||||
if len(data) < 4 {
|
|
||||||
messages.Warn("Transaction data is not valid ABI (missing the 4 byte call prefix)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n := len(data) - 4; n%32 != 0 {
|
|
||||||
messages.Warn(fmt.Sprintf("Transaction data is not valid ABI (length should be a multiple of 32 (was %d))", n))
|
|
||||||
}
|
|
||||||
// If a custom method selector was provided, validate with that
|
|
||||||
if selector != nil {
|
|
||||||
if info, err := verifySelector(*selector, data); err != nil {
|
|
||||||
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be matched: %v", err))
|
|
||||||
} else {
|
|
||||||
messages.Info(fmt.Sprintf("Transaction invokes the following method: %q", info.String()))
|
|
||||||
db.AddSelector(*selector, data[:4])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// No method selector was provided, check the database for embedded ones
|
|
||||||
embedded, err := db.Selector(data[:4])
|
|
||||||
if err != nil {
|
|
||||||
messages.Warn(fmt.Sprintf("Transaction contains data, but the ABI signature could not be found: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info, err := verifySelector(embedded, data); err != nil {
|
|
||||||
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be verified: %v", err))
|
|
||||||
} else {
|
|
||||||
messages.Info(fmt.Sprintf("Transaction invokes the following method: %q", info.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
// Copyright 2019 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 fourbyte
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mixAddr(a string) (*common.MixedcaseAddress, error) {
|
|
||||||
return common.NewMixedcaseAddressFromString(a)
|
|
||||||
}
|
|
||||||
func toHexBig(h string) hexutil.Big {
|
|
||||||
b := new(big.Int).SetBytes(common.FromHex(h))
|
|
||||||
return hexutil.Big(*b)
|
|
||||||
}
|
|
||||||
func toHexUint(h string) hexutil.Uint64 {
|
|
||||||
b := new(big.Int).SetBytes(common.FromHex(h))
|
|
||||||
return hexutil.Uint64(b.Uint64())
|
|
||||||
}
|
|
||||||
func dummyTxArgs(t txtestcase) *apitypes.SendTxArgs {
|
|
||||||
to, _ := mixAddr(t.to)
|
|
||||||
from, _ := mixAddr(t.from)
|
|
||||||
n := toHexUint(t.n)
|
|
||||||
gas := toHexUint(t.g)
|
|
||||||
gasPrice := toHexBig(t.gp)
|
|
||||||
value := toHexBig(t.value)
|
|
||||||
var (
|
|
||||||
data, input *hexutil.Bytes
|
|
||||||
)
|
|
||||||
if t.d != "" {
|
|
||||||
a := hexutil.Bytes(common.FromHex(t.d))
|
|
||||||
data = &a
|
|
||||||
}
|
|
||||||
if t.i != "" {
|
|
||||||
a := hexutil.Bytes(common.FromHex(t.i))
|
|
||||||
input = &a
|
|
||||||
}
|
|
||||||
return &apitypes.SendTxArgs{
|
|
||||||
From: *from,
|
|
||||||
To: to,
|
|
||||||
Value: value,
|
|
||||||
Nonce: n,
|
|
||||||
GasPrice: &gasPrice,
|
|
||||||
Gas: gas,
|
|
||||||
Data: data,
|
|
||||||
Input: input,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type txtestcase struct {
|
|
||||||
from, to, n, g, gp, value, d, i string
|
|
||||||
expectErr bool
|
|
||||||
numMessages int
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTransactionValidation(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
var (
|
|
||||||
// use empty db, there are other tests for the abi-specific stuff
|
|
||||||
db = newEmpty()
|
|
||||||
)
|
|
||||||
testcases := []txtestcase{
|
|
||||||
// Invalid to checksum
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "000000000000000000000000000000000000dead",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
|
|
||||||
// valid 0x000000000000000000000000000000000000dEaD
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 0},
|
|
||||||
// conflicting input and data
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", i: "0x02", expectErr: true},
|
|
||||||
// Data can't be parsed
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x0102", numMessages: 1},
|
|
||||||
// Data (on Input) can't be parsed
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", i: "0x0102", numMessages: 1},
|
|
||||||
// Send to 0
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "0x0000000000000000000000000000000000000000",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
|
|
||||||
// Create empty contract (no value)
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x00", numMessages: 1},
|
|
||||||
// Create empty contract (with value)
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", expectErr: true},
|
|
||||||
// Small payload for create
|
|
||||||
{from: "000000000000000000000000000000000000dead", to: "",
|
|
||||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", numMessages: 1},
|
|
||||||
}
|
|
||||||
for i, test := range testcases {
|
|
||||||
msgs, err := db.ValidateTransaction(nil, dummyTxArgs(test))
|
|
||||||
if err == nil && test.expectErr {
|
|
||||||
t.Errorf("Test %d, expected error", i)
|
|
||||||
for _, msg := range msgs.Messages {
|
|
||||||
t.Logf("* %s: %s", msg.Typ, msg.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil && !test.expectErr {
|
|
||||||
t.Errorf("Test %d, unexpected error: %v", i, err)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
got := len(msgs.Messages)
|
|
||||||
if got != test.numMessages {
|
|
||||||
for _, msg := range msgs.Messages {
|
|
||||||
t.Logf("* %s: %s", msg.Typ, msg.Message)
|
|
||||||
}
|
|
||||||
t.Errorf("Test %d, expected %d messages, got %d", i, test.numMessages, got)
|
|
||||||
} else {
|
|
||||||
//Debug printout, remove later
|
|
||||||
for _, msg := range msgs.Messages {
|
|
||||||
t.Logf("* [%d] %s: %s", i, msg.Typ, msg.Message)
|
|
||||||
}
|
|
||||||
t.Log()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
// Copyright 2018 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 rules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre/deps"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// consoleOutput is an override for the console.log and console.error methods to
|
|
||||||
// stream the output into the configured output stream instead of stdout.
|
|
||||||
func consoleOutput(call goja.FunctionCall) goja.Value {
|
|
||||||
output := []string{"JS:> "}
|
|
||||||
for _, argument := range call.Arguments {
|
|
||||||
output = append(output, fmt.Sprintf("%v", argument))
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, strings.Join(output, " "))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
// rulesetUI provides an implementation of UIClientAPI that evaluates a javascript
|
|
||||||
// file for each defined UI-method
|
|
||||||
type rulesetUI struct {
|
|
||||||
next core.UIClientAPI // The next handler, for manual processing
|
|
||||||
storage storage.Storage
|
|
||||||
jsRules string // The rules to use
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleEvaluator(next core.UIClientAPI, jsbackend storage.Storage) (*rulesetUI, error) {
|
|
||||||
c := &rulesetUI{
|
|
||||||
next: next,
|
|
||||||
storage: jsbackend,
|
|
||||||
jsRules: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
func (r *rulesetUI) RegisterUIServer(api *core.UIServerAPI) {
|
|
||||||
r.next.RegisterUIServer(api)
|
|
||||||
// TODO, make it possible to query from js
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) Init(javascriptRules string) error {
|
|
||||||
r.jsRules = javascriptRules
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (goja.Value, error) {
|
|
||||||
// Instantiate a fresh vm engine every time
|
|
||||||
vm := goja.New()
|
|
||||||
|
|
||||||
// Set the native callbacks
|
|
||||||
consoleObj := vm.NewObject()
|
|
||||||
consoleObj.Set("log", consoleOutput)
|
|
||||||
consoleObj.Set("error", consoleOutput)
|
|
||||||
vm.Set("console", consoleObj)
|
|
||||||
|
|
||||||
storageObj := vm.NewObject()
|
|
||||||
storageObj.Set("put", func(call goja.FunctionCall) goja.Value {
|
|
||||||
key, val := call.Argument(0).String(), call.Argument(1).String()
|
|
||||||
if val == "" {
|
|
||||||
r.storage.Del(key)
|
|
||||||
} else {
|
|
||||||
r.storage.Put(key, val)
|
|
||||||
}
|
|
||||||
return goja.Null()
|
|
||||||
})
|
|
||||||
storageObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
|
||||||
goval, _ := r.storage.Get(call.Argument(0).String())
|
|
||||||
jsval := vm.ToValue(goval)
|
|
||||||
return jsval
|
|
||||||
})
|
|
||||||
vm.Set("storage", storageObj)
|
|
||||||
|
|
||||||
// Load bootstrap libraries
|
|
||||||
script, err := goja.Compile("bignumber.js", deps.BigNumberJS, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed loading libraries", "err", err)
|
|
||||||
return goja.Undefined(), err
|
|
||||||
}
|
|
||||||
vm.RunProgram(script)
|
|
||||||
|
|
||||||
// Run the actual rule implementation
|
|
||||||
_, err = vm.RunString(r.jsRules)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Execution failed", "err", err)
|
|
||||||
return goja.Undefined(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// And the actual call
|
|
||||||
// All calls are objects with the parameters being keys in that object.
|
|
||||||
// To provide additional insulation between js and go, we serialize it into JSON on the Go-side,
|
|
||||||
// and deserialize it on the JS side.
|
|
||||||
|
|
||||||
jsonbytes, err := json.Marshal(jsarg)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("failed marshalling data", "data", jsarg)
|
|
||||||
return goja.Undefined(), err
|
|
||||||
}
|
|
||||||
// Now, we call foobar(JSON.parse(<jsondata>)).
|
|
||||||
var call string
|
|
||||||
if len(jsonbytes) > 0 {
|
|
||||||
call = fmt.Sprintf("%v(JSON.parse(%v))", jsfunc, string(jsonbytes))
|
|
||||||
} else {
|
|
||||||
call = fmt.Sprintf("%v()", jsfunc)
|
|
||||||
}
|
|
||||||
return vm.RunString(call)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) checkApproval(jsfunc string, jsarg []byte, err error) (bool, error) {
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
v, err := r.execute(jsfunc, string(jsarg))
|
|
||||||
if err != nil {
|
|
||||||
log.Info("error occurred during execution", "error", err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
result := v.ToString().String()
|
|
||||||
if result == "Approve" {
|
|
||||||
log.Info("Op approved")
|
|
||||||
return true, nil
|
|
||||||
} else if result == "Reject" {
|
|
||||||
log.Info("Op rejected")
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, errors.New("unknown response")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
|
||||||
jsonreq, err := json.Marshal(request)
|
|
||||||
approved, err := r.checkApproval("ApproveTx", jsonreq, err)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Rule-based approval error, going to manual", "error", err)
|
|
||||||
return r.next.ApproveTx(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
if approved {
|
|
||||||
return core.SignTxResponse{
|
|
||||||
Transaction: request.Transaction,
|
|
||||||
Approved: true},
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
return core.SignTxResponse{Approved: false}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
|
||||||
jsonreq, err := json.Marshal(request)
|
|
||||||
approved, err := r.checkApproval("ApproveSignData", jsonreq, err)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Rule-based approval error, going to manual", "error", err)
|
|
||||||
return r.next.ApproveSignData(request)
|
|
||||||
}
|
|
||||||
if approved {
|
|
||||||
return core.SignDataResponse{Approved: true}, nil
|
|
||||||
}
|
|
||||||
return core.SignDataResponse{Approved: false}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnInputRequired not handled by rules
|
|
||||||
func (r *rulesetUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
|
||||||
return r.next.OnInputRequired(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
|
||||||
jsonreq, err := json.Marshal(request)
|
|
||||||
approved, err := r.checkApproval("ApproveListing", jsonreq, err)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Rule-based approval error, going to manual", "error", err)
|
|
||||||
return r.next.ApproveListing(request)
|
|
||||||
}
|
|
||||||
if approved {
|
|
||||||
return core.ListResponse{Accounts: request.Accounts}, nil
|
|
||||||
}
|
|
||||||
return core.ListResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
|
||||||
// This cannot be handled by rules, requires setting a password
|
|
||||||
// dispatch to next
|
|
||||||
return r.next.ApproveNewAccount(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ShowError(message string) {
|
|
||||||
log.Error(message)
|
|
||||||
r.next.ShowError(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) ShowInfo(message string) {
|
|
||||||
log.Info(message)
|
|
||||||
r.next.ShowInfo(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) {
|
|
||||||
jsonInfo, err := json.Marshal(info)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("failed marshalling data", "data", info)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.next.OnSignerStartup(info)
|
|
||||||
_, err = r.execute("OnSignerStartup", string(jsonInfo))
|
|
||||||
if err != nil {
|
|
||||||
log.Info("error occurred during execution", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
jsonTx, err := json.Marshal(tx)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("failed marshalling transaction", "tx", tx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = r.execute("OnApprovedTx", string(jsonTx))
|
|
||||||
if err != nil {
|
|
||||||
log.Info("error occurred during execution", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,626 +0,0 @@
|
||||||
// Copyright 2018 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 rules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
||||||
"github.com/ethereum/go-ethereum/signer/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
const JS = `
|
|
||||||
/**
|
|
||||||
This is an example implementation of a Javascript rule file.
|
|
||||||
|
|
||||||
When the signer receives a request over the external API, the corresponding method is evaluated.
|
|
||||||
Three things can happen:
|
|
||||||
|
|
||||||
1. The method returns "Approve". This means the operation is permitted.
|
|
||||||
2. The method returns "Reject". This means the operation is rejected.
|
|
||||||
3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
|
|
||||||
that the operation will continue to manual processing, via the regular UI method chosen by the user.
|
|
||||||
|
|
||||||
[*] Note: Future version of the ruleset may use more complex json-based return values, making it possible to not
|
|
||||||
only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
|
|
||||||
accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
|
|
||||||
|
|
||||||
**/
|
|
||||||
|
|
||||||
function ApproveListing(request){
|
|
||||||
console.log("In js approve listing");
|
|
||||||
console.log(request.accounts[3].Address)
|
|
||||||
console.log(request.meta.Remote)
|
|
||||||
return "Approve"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApproveTx(request){
|
|
||||||
console.log("test");
|
|
||||||
console.log("from");
|
|
||||||
return "Reject";
|
|
||||||
}
|
|
||||||
|
|
||||||
function test(thing){
|
|
||||||
console.log(thing.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
func mixAddr(a string) (*common.MixedcaseAddress, error) {
|
|
||||||
return common.NewMixedcaseAddressFromString(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
type alwaysDenyUI struct{}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
|
||||||
return core.UserInputResponse{}, nil
|
|
||||||
}
|
|
||||||
func (alwaysDenyUI) RegisterUIServer(api *core.UIServerAPI) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
|
||||||
return core.SignTxResponse{Transaction: request.Transaction, Approved: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
|
||||||
return core.SignDataResponse{Approved: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
|
||||||
return core.ListResponse{Accounts: nil}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
|
||||||
return core.NewAccountResponse{Approved: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ShowError(message string) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) ShowInfo(message string) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRuleEngine(js string) (*rulesetUI, error) {
|
|
||||||
r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create js engine: %v", err)
|
|
||||||
}
|
|
||||||
if err = r.Init(js); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListRequest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
accs := make([]accounts.Account, 5)
|
|
||||||
|
|
||||||
for i := range accs {
|
|
||||||
addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
|
|
||||||
acc := accounts.Account{
|
|
||||||
Address: common.BytesToAddress(common.Hex2Bytes(addr)),
|
|
||||||
URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
|
|
||||||
}
|
|
||||||
accs[i] = acc
|
|
||||||
}
|
|
||||||
|
|
||||||
js := `function ApproveListing(){ return "Approve" }`
|
|
||||||
|
|
||||||
r, err := initRuleEngine(js)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, _ := r.ApproveListing(&core.ListRequest{
|
|
||||||
Accounts: accs,
|
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
|
||||||
})
|
|
||||||
if len(resp.Accounts) != len(accs) {
|
|
||||||
t.Errorf("Expected check to resolve to 'Approve'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignTxRequest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
js := `
|
|
||||||
function ApproveTx(r){
|
|
||||||
console.log("transaction.from", r.transaction.from);
|
|
||||||
console.log("transaction.to", r.transaction.to);
|
|
||||||
console.log("transaction.value", r.transaction.value);
|
|
||||||
console.log("transaction.nonce", r.transaction.nonce);
|
|
||||||
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
|
|
||||||
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
|
|
||||||
}`
|
|
||||||
|
|
||||||
r, err := initRuleEngine(js)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
to, err := mixAddr("000000000000000000000000000000000000dead")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
from, err := mixAddr("0000000000000000000000000000000000001337")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Logf("to %v", to.Address().String())
|
|
||||||
resp, err := r.ApproveTx(&core.SignTxRequest{
|
|
||||||
Transaction: apitypes.SendTxArgs{
|
|
||||||
From: *from,
|
|
||||||
To: to},
|
|
||||||
Callinfo: nil,
|
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Approved {
|
|
||||||
t.Errorf("Expected check to resolve to 'Approve'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dummyUI struct {
|
|
||||||
calls []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
|
||||||
d.calls = append(d.calls, "OnInputRequired")
|
|
||||||
return core.UserInputResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
|
||||||
d.calls = append(d.calls, "ApproveTx")
|
|
||||||
return core.SignTxResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
|
||||||
d.calls = append(d.calls, "ApproveSignData")
|
|
||||||
return core.SignDataResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
|
||||||
d.calls = append(d.calls, "ApproveListing")
|
|
||||||
return core.ListResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
|
||||||
d.calls = append(d.calls, "ApproveNewAccount")
|
|
||||||
return core.NewAccountResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ShowError(message string) {
|
|
||||||
d.calls = append(d.calls, "ShowError")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) ShowInfo(message string) {
|
|
||||||
d.calls = append(d.calls, "ShowInfo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
d.calls = append(d.calls, "OnApprovedTx")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
|
|
||||||
func TestForwarding(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
js := ""
|
|
||||||
ui := &dummyUI{make([]string, 0)}
|
|
||||||
jsBackend := storage.NewEphemeralStorage()
|
|
||||||
r, err := NewRuleEvaluator(ui, jsBackend)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create js engine: %v", err)
|
|
||||||
}
|
|
||||||
if err = r.Init(js); err != nil {
|
|
||||||
t.Fatalf("Failed to load bootstrap js: %v", err)
|
|
||||||
}
|
|
||||||
r.ApproveSignData(nil)
|
|
||||||
r.ApproveTx(nil)
|
|
||||||
r.ApproveNewAccount(nil)
|
|
||||||
r.ApproveListing(nil)
|
|
||||||
r.ShowError("test")
|
|
||||||
r.ShowInfo("test")
|
|
||||||
|
|
||||||
//This one is not forwarded
|
|
||||||
r.OnApprovedTx(ethapi.SignTransactionResult{})
|
|
||||||
|
|
||||||
expCalls := 6
|
|
||||||
if len(ui.calls) != expCalls {
|
|
||||||
t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMissingFunc(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
r, err := initRuleEngine(JS)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = r.execute("MissingMethod", "test")
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
approved, err := r.checkApproval("MissingMethod", nil, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected missing method to yield error'")
|
|
||||||
}
|
|
||||||
if approved {
|
|
||||||
t.Errorf("Expected missing method to cause non-approval")
|
|
||||||
}
|
|
||||||
t.Logf("Err %v", err)
|
|
||||||
}
|
|
||||||
func TestStorage(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
js := `
|
|
||||||
function testStorage(){
|
|
||||||
storage.put("mykey", "myvalue")
|
|
||||||
a = storage.get("mykey")
|
|
||||||
|
|
||||||
storage.put("mykey", ["a", "list"]) // Should result in "a,list"
|
|
||||||
a += storage.get("mykey")
|
|
||||||
|
|
||||||
|
|
||||||
storage.put("mykey", {"an": "object"}) // Should result in "[object Object]"
|
|
||||||
a += storage.get("mykey")
|
|
||||||
|
|
||||||
|
|
||||||
storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
|
|
||||||
a += storage.get("mykey")
|
|
||||||
|
|
||||||
a += storage.get("missingkey") //Missing keys should result in empty string
|
|
||||||
storage.put("","missing key==noop") // Can't store with 0-length key
|
|
||||||
a += storage.get("") // Should result in ''
|
|
||||||
|
|
||||||
var b = new BigNumber(2)
|
|
||||||
var c = new BigNumber(16)//"0xf0",16)
|
|
||||||
var d = b.plus(c)
|
|
||||||
console.log(d)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
`
|
|
||||||
r, err := initRuleEngine(js)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := r.execute("testStorage", nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
retval := v.ToString().String()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
exp := `myvaluea,list[object Object]{"an":"object"}`
|
|
||||||
if retval != exp {
|
|
||||||
t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
|
|
||||||
}
|
|
||||||
t.Logf("Err %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExampleTxWindow = `
|
|
||||||
function big(str){
|
|
||||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
|
|
||||||
return new BigNumber(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time window: 1 week
|
|
||||||
var window = 1000* 3600*24*7;
|
|
||||||
|
|
||||||
// Limit : 1 ether
|
|
||||||
var limit = new BigNumber("1e18");
|
|
||||||
|
|
||||||
function isLimitOk(transaction){
|
|
||||||
var value = big(transaction.value)
|
|
||||||
// Start of our window function
|
|
||||||
var windowstart = new Date().getTime() - window;
|
|
||||||
|
|
||||||
var txs = [];
|
|
||||||
var stored = storage.get('txs');
|
|
||||||
|
|
||||||
if(stored != ""){
|
|
||||||
txs = JSON.parse(stored)
|
|
||||||
}
|
|
||||||
// First, remove all that have passed out of the time-window
|
|
||||||
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
|
|
||||||
console.log(txs, newtxs.length);
|
|
||||||
|
|
||||||
// Secondly, aggregate the current sum
|
|
||||||
sum = new BigNumber(0)
|
|
||||||
|
|
||||||
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
|
|
||||||
console.log("ApproveTx > Sum so far", sum);
|
|
||||||
console.log("ApproveTx > Requested", value.toNumber());
|
|
||||||
|
|
||||||
// Would we exceed weekly limit ?
|
|
||||||
return sum.plus(value).lt(limit)
|
|
||||||
|
|
||||||
}
|
|
||||||
function ApproveTx(r){
|
|
||||||
console.log(r)
|
|
||||||
console.log(typeof(r))
|
|
||||||
if (isLimitOk(r.transaction)){
|
|
||||||
return "Approve"
|
|
||||||
}
|
|
||||||
return "Nope"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
|
|
||||||
* 'response_str' contains the return value that will be sent to the external caller.
|
|
||||||
* The return value from this method is ignore - the reason for having this callback is to allow the
|
|
||||||
* ruleset to keep track of approved transactions.
|
|
||||||
*
|
|
||||||
* When implementing rate-limited rules, this callback should be used.
|
|
||||||
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
|
|
||||||
* then accepts the transaction, this method will be called.
|
|
||||||
*
|
|
||||||
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
|
|
||||||
*/
|
|
||||||
function OnApprovedTx(resp){
|
|
||||||
var value = big(resp.tx.value)
|
|
||||||
var txs = []
|
|
||||||
// Load stored transactions
|
|
||||||
var stored = storage.get('txs');
|
|
||||||
if(stored != ""){
|
|
||||||
txs = JSON.parse(stored)
|
|
||||||
}
|
|
||||||
// Add this to the storage
|
|
||||||
txs.push({tstamp: new Date().getTime(), value: value});
|
|
||||||
storage.put("txs", JSON.stringify(txs));
|
|
||||||
}
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
func dummyTx(value hexutil.Big) *core.SignTxRequest {
|
|
||||||
to, _ := mixAddr("000000000000000000000000000000000000dead")
|
|
||||||
from, _ := mixAddr("000000000000000000000000000000000000dead")
|
|
||||||
n := hexutil.Uint64(3)
|
|
||||||
gas := hexutil.Uint64(21000)
|
|
||||||
gasPrice := hexutil.Big(*big.NewInt(2000000))
|
|
||||||
|
|
||||||
return &core.SignTxRequest{
|
|
||||||
Transaction: apitypes.SendTxArgs{
|
|
||||||
From: *from,
|
|
||||||
To: to,
|
|
||||||
Value: value,
|
|
||||||
Nonce: n,
|
|
||||||
GasPrice: &gasPrice,
|
|
||||||
Gas: gas,
|
|
||||||
},
|
|
||||||
Callinfo: []apitypes.ValidationInfo{
|
|
||||||
{Typ: "Warning", Message: "All your base are belong to us"},
|
|
||||||
},
|
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dummyTxWithV(value uint64) *core.SignTxRequest {
|
|
||||||
v := new(big.Int).SetUint64(value)
|
|
||||||
h := hexutil.Big(*v)
|
|
||||||
return dummyTx(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dummySigned(value *big.Int) *types.Transaction {
|
|
||||||
to := common.HexToAddress("000000000000000000000000000000000000dead")
|
|
||||||
gas := uint64(21000)
|
|
||||||
gasPrice := big.NewInt(2000000)
|
|
||||||
data := make([]byte, 0)
|
|
||||||
return types.NewTransaction(3, to, value, gas, gasPrice, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLimitWindow(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
r, err := initRuleEngine(ExampleTxWindow)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 0.3 ether: 429D069189E0000 wei
|
|
||||||
v := new(big.Int).SetBytes(common.Hex2Bytes("0429D069189E0000"))
|
|
||||||
h := hexutil.Big(*v)
|
|
||||||
// The first three should succeed
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
unsigned := dummyTx(h)
|
|
||||||
resp, err := r.ApproveTx(unsigned)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Approved {
|
|
||||||
t.Errorf("Expected check to resolve to 'Approve'")
|
|
||||||
}
|
|
||||||
// Create a dummy signed transaction
|
|
||||||
|
|
||||||
response := ethapi.SignTransactionResult{
|
|
||||||
Tx: dummySigned(v),
|
|
||||||
Raw: common.Hex2Bytes("deadbeef"),
|
|
||||||
}
|
|
||||||
r.OnApprovedTx(response)
|
|
||||||
}
|
|
||||||
// Fourth should fail
|
|
||||||
resp, _ := r.ApproveTx(dummyTx(h))
|
|
||||||
if resp.Approved {
|
|
||||||
t.Errorf("Expected check to resolve to 'Reject'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
|
|
||||||
type dontCallMe struct {
|
|
||||||
t *testing.T
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
return core.UserInputResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
return core.SignTxResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
return core.SignDataResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
return core.ListResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
return core.NewAccountResponse{}, core.ErrRequestDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ShowError(message string) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) ShowInfo(message string) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
|
|
||||||
d.t.Fatalf("Did not expect next-handler to be called")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
|
|
||||||
// if it does, that would be bad since developers may rely on that to store data,
|
|
||||||
// instead of using the disk-based data storage
|
|
||||||
func TestContextIsCleared(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
js := `
|
|
||||||
function ApproveTx(){
|
|
||||||
if (typeof foobar == 'undefined') {
|
|
||||||
foobar = "Approve"
|
|
||||||
}
|
|
||||||
console.log(foobar)
|
|
||||||
if (foobar == "Approve"){
|
|
||||||
foobar = "Reject"
|
|
||||||
}else{
|
|
||||||
foobar = "Approve"
|
|
||||||
}
|
|
||||||
return foobar
|
|
||||||
}
|
|
||||||
`
|
|
||||||
ui := &dontCallMe{t}
|
|
||||||
r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create js engine: %v", err)
|
|
||||||
}
|
|
||||||
if err = r.Init(js); err != nil {
|
|
||||||
t.Fatalf("Failed to load bootstrap js: %v", err)
|
|
||||||
}
|
|
||||||
tx := dummyTxWithV(0)
|
|
||||||
r1, _ := r.ApproveTx(tx)
|
|
||||||
r2, _ := r.ApproveTx(tx)
|
|
||||||
if r1.Approved != r2.Approved {
|
|
||||||
t.Errorf("Expected execution context to be cleared between executions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignData(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
js := `function ApproveListing(){
|
|
||||||
return "Approve"
|
|
||||||
}
|
|
||||||
function ApproveSignData(r){
|
|
||||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
|
||||||
{
|
|
||||||
if(r.messages[0].value.indexOf("bazonk") >= 0){
|
|
||||||
return "Approve"
|
|
||||||
}
|
|
||||||
return "Reject"
|
|
||||||
}
|
|
||||||
// Otherwise goes to manual processing
|
|
||||||
}`
|
|
||||||
r, err := initRuleEngine(js)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message := "baz bazonk foo"
|
|
||||||
hash, rawdata := accounts.TextAndHash([]byte(message))
|
|
||||||
addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
|
||||||
|
|
||||||
t.Logf("address %v %v\n", addr.String(), addr.Original())
|
|
||||||
|
|
||||||
nvt := []*apitypes.NameValueType{
|
|
||||||
{
|
|
||||||
Name: "message",
|
|
||||||
Typ: "text/plain",
|
|
||||||
Value: message,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
resp, err := r.ApproveSignData(&core.SignDataRequest{
|
|
||||||
Address: *addr,
|
|
||||||
Messages: nvt,
|
|
||||||
Hash: hash,
|
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
|
||||||
Rawdata: []byte(rawdata),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Approved {
|
|
||||||
t.Fatalf("Expected approved")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
// Copyright 2018 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 storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type storedCredential struct {
|
|
||||||
// The iv
|
|
||||||
Iv []byte `json:"iv"`
|
|
||||||
// The ciphertext
|
|
||||||
CipherText []byte `json:"c"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AESEncryptedStorage is a storage type which is backed by a json-file. The json-file contains
|
|
||||||
// key-value mappings, where the keys are _not_ encrypted, only the values are.
|
|
||||||
type AESEncryptedStorage struct {
|
|
||||||
// File to read/write credentials
|
|
||||||
filename string
|
|
||||||
// Key stored in base64
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
|
|
||||||
func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
|
|
||||||
return &AESEncryptedStorage{
|
|
||||||
filename: filename,
|
|
||||||
key: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
|
||||||
func (s *AESEncryptedStorage) Put(key, value string) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := s.readEncryptedStorage()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ciphertext, iv, err := encrypt(s.key, []byte(value), []byte(key))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to encrypt entry", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
encrypted := storedCredential{Iv: iv, CipherText: ciphertext}
|
|
||||||
data[key] = encrypted
|
|
||||||
if err = s.writeEncryptedStorage(data); err != nil {
|
|
||||||
log.Warn("Failed to write entry", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if it does not exist or
|
|
||||||
// key is of 0-length.
|
|
||||||
func (s *AESEncryptedStorage) Get(key string) (string, error) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return "", ErrZeroKey
|
|
||||||
}
|
|
||||||
data, err := s.readEncryptedStorage()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
encrypted, exist := data[key]
|
|
||||||
if !exist {
|
|
||||||
log.Warn("Key does not exist", "key", key)
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to decrypt key", "key", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(entry), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
|
||||||
func (s *AESEncryptedStorage) Del(key string) {
|
|
||||||
data, err := s.readEncryptedStorage()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(data, key)
|
|
||||||
if err = s.writeEncryptedStorage(data); err != nil {
|
|
||||||
log.Warn("Failed to write entry", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readEncryptedStorage reads the file with encrypted creds
|
|
||||||
func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]storedCredential, error) {
|
|
||||||
creds := make(map[string]storedCredential)
|
|
||||||
raw, err := os.ReadFile(s.filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
// Doesn't exist yet
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(raw, &creds); err != nil {
|
|
||||||
log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeEncryptedStorage write the file with encrypted creds
|
|
||||||
func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]storedCredential) error {
|
|
||||||
raw, err := json.Marshal(creds)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = os.WriteFile(s.filename, raw, 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// encrypt encrypts plaintext with the given key, with additional data
|
|
||||||
// The 'additionalData' is used to place the (plaintext) KV-store key into the V,
|
|
||||||
// to prevent the possibility to alter a K, or swap two entries in the KV store with each other.
|
|
||||||
func encrypt(key []byte, plaintext []byte, additionalData []byte) ([]byte, []byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
aesgcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
nonce := make([]byte, aesgcm.NonceSize())
|
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
ciphertext := aesgcm.Seal(nil, nonce, plaintext, additionalData)
|
|
||||||
return ciphertext, nonce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decrypt(key []byte, nonce []byte, ciphertext []byte, additionalData []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
aesgcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, additionalData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return plaintext, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
// Copyright 2018 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 storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/mattn/go-colorable"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEncryption(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// key := []byte("AES256Key-32Characters1234567890")
|
|
||||||
// plaintext := []byte(value)
|
|
||||||
key := []byte("AES256Key-32Characters1234567890")
|
|
||||||
plaintext := []byte("exampleplaintext")
|
|
||||||
|
|
||||||
c, iv, err := encrypt(key, plaintext, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Logf("Ciphertext %x, nonce %x\n", c, iv)
|
|
||||||
|
|
||||||
p, err := decrypt(key, iv, c, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Logf("Plaintext %v\n", string(p))
|
|
||||||
if !bytes.Equal(plaintext, p) {
|
|
||||||
t.Errorf("Failed: expected plaintext recovery, got %v expected %v", string(plaintext), string(p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileStorage(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
a := map[string]storedCredential{
|
|
||||||
"secret": {
|
|
||||||
Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"),
|
|
||||||
CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"),
|
|
||||||
},
|
|
||||||
"secret2": {
|
|
||||||
Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"),
|
|
||||||
CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
d := t.TempDir()
|
|
||||||
stored := &AESEncryptedStorage{
|
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
|
||||||
stored.writeEncryptedStorage(a)
|
|
||||||
read := &AESEncryptedStorage{
|
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
|
||||||
creds, err := read.readEncryptedStorage()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for k, v := range a {
|
|
||||||
if v2, exist := creds[k]; !exist {
|
|
||||||
t.Errorf("Missing entry %v", k)
|
|
||||||
} else {
|
|
||||||
if !bytes.Equal(v.CipherText, v2.CipherText) {
|
|
||||||
t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(v.Iv, v2.Iv) {
|
|
||||||
t.Errorf("Wrong iv")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestEnd2End(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.LevelInfo, true)))
|
|
||||||
|
|
||||||
d := t.TempDir()
|
|
||||||
|
|
||||||
s1 := &AESEncryptedStorage{
|
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
|
||||||
s2 := &AESEncryptedStorage{
|
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
|
||||||
|
|
||||||
s1.Put("bazonk", "foobar")
|
|
||||||
if v, err := s2.Get("bazonk"); v != "foobar" || err != nil {
|
|
||||||
t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSwappedKeys(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// It should not be possible to swap the keys/values, so that
|
|
||||||
// K1:V1, K2:V2 can be swapped into K1:V2, K2:V1
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.LevelInfo, true)))
|
|
||||||
|
|
||||||
d := t.TempDir()
|
|
||||||
|
|
||||||
s1 := &AESEncryptedStorage{
|
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
|
||||||
s1.Put("k1", "v1")
|
|
||||||
s1.Put("k2", "v2")
|
|
||||||
// Now make a modified copy
|
|
||||||
|
|
||||||
creds := make(map[string]storedCredential)
|
|
||||||
raw, err := os.ReadFile(s1.filename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(raw, &creds); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
swap := func() {
|
|
||||||
// Turn it into K1:V2, K2:V2
|
|
||||||
v1, v2 := creds["k1"], creds["k2"]
|
|
||||||
creds["k2"], creds["k1"] = v1, v2
|
|
||||||
raw, err = json.Marshal(creds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err = os.WriteFile(s1.filename, raw, 0600); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
swap()
|
|
||||||
if v, _ := s1.Get("k1"); v != "" {
|
|
||||||
t.Errorf("swapped value should return empty")
|
|
||||||
}
|
|
||||||
swap()
|
|
||||||
if v, _ := s1.Get("k1"); v != "v1" {
|
|
||||||
t.Errorf("double-swapped value should work fine")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
// Copyright 2018 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 storage
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrZeroKey is returned if an attempt was made to inset a 0-length key.
|
|
||||||
ErrZeroKey = errors.New("0-length key")
|
|
||||||
|
|
||||||
// ErrNotFound is returned if an unknown key is attempted to be retrieved.
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage interface {
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
|
||||||
Put(key, value string)
|
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if the key is 0-length
|
|
||||||
// or unknown.
|
|
||||||
Get(key string) (string, error)
|
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
|
||||||
Del(key string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EphemeralStorage is an in-memory storage that does
|
|
||||||
// not persist values to disk. Mainly used for testing
|
|
||||||
type EphemeralStorage struct {
|
|
||||||
data map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
|
||||||
func (s *EphemeralStorage) Put(key, value string) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if the key is 0-length
|
|
||||||
// or unknown.
|
|
||||||
func (s *EphemeralStorage) Get(key string) (string, error) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return "", ErrZeroKey
|
|
||||||
}
|
|
||||||
if v, ok := s.data[key]; ok {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
|
||||||
func (s *EphemeralStorage) Del(key string) {
|
|
||||||
delete(s.data, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEphemeralStorage() Storage {
|
|
||||||
s := &EphemeralStorage{
|
|
||||||
data: make(map[string]string),
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoStorage is a dummy construct which doesn't remember anything you tell it
|
|
||||||
type NoStorage struct{}
|
|
||||||
|
|
||||||
func (s *NoStorage) Put(key, value string) {}
|
|
||||||
func (s *NoStorage) Del(key string) {}
|
|
||||||
func (s *NoStorage) Get(key string) (string, error) {
|
|
||||||
return "", errors.New("missing key, I probably forgot")
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue