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