mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-18 09:53:48 +00:00
signer, rules, storage: implement rules + ephemeral storage for signer rules
This commit is contained in:
parent
3a9bfba76a
commit
da312a1dcb
6 changed files with 810 additions and 0 deletions
4
cmd/signer/rules/deps/bignumber.js
Normal file
4
cmd/signer/rules/deps/bignumber.js
Normal file
File diff suppressed because one or more lines are too long
235
cmd/signer/rules/deps/bindata.go
Normal file
235
cmd/signer/rules/deps/bindata.go
Normal file
File diff suppressed because one or more lines are too long
21
cmd/signer/rules/deps/deps.go
Normal file
21
cmd/signer/rules/deps/deps.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2017 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 deps contains the console JavaScript dependencies Go embedded.
|
||||
package deps
|
||||
|
||||
//go:generate go-bindata -nometadata -pkg deps -o bindata.go bignumber.js
|
||||
//go:generate gofmt -w -s bindata.go
|
||||
158
cmd/signer/rules/rules.go
Normal file
158
cmd/signer/rules/rules.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/cmd/signer"
|
||||
"github.com/ethereum/go-ethereum/cmd/signer/rules/deps"
|
||||
"github.com/ethereum/go-ethereum/cmd/signer/storage"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/robertkrimen/otto"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
BigNumber_JS = deps.MustAsset("bignumber.js")
|
||||
)
|
||||
|
||||
// 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 otto.FunctionCall) otto.Value {
|
||||
output := []string{"JS:> "}
|
||||
for _, argument := range call.ArgumentList {
|
||||
output = append(output, fmt.Sprintf("%v", argument))
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, strings.Join(output, " "))
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
// rulesetUi provides an implementation of SignerUI that evaluates a javascript
|
||||
// file for each defined UI-method
|
||||
type rulesetUi struct {
|
||||
vm *otto.Otto // The JS vm
|
||||
next signer.SignerUI // The next handler, for manual processing
|
||||
storage storage.Storage
|
||||
}
|
||||
|
||||
func NewRuleEvaluator() (*rulesetUi, error) {
|
||||
c := &rulesetUi{
|
||||
vm: otto.New(),
|
||||
storage: storage.NewEphemeralStorage(),
|
||||
}
|
||||
consoleObj, _ := c.vm.Get("console")
|
||||
consoleObj.Object().Set("log", consoleOutput)
|
||||
consoleObj.Object().Set("error", consoleOutput)
|
||||
|
||||
c.vm.Set("storage", c.storage)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (r *rulesetUi) Init(javascriptRules string) error {
|
||||
script, err := r.vm.Compile("bignumber.js", BigNumber_JS)
|
||||
if err != nil {
|
||||
log.Warn("Failed loading libraries", "err", err)
|
||||
return err
|
||||
}
|
||||
r.vm.Run(script)
|
||||
|
||||
_, err = r.vm.Run(javascriptRules)
|
||||
if err != nil {
|
||||
log.Warn("Execution failed", "err", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := r.vm.Call("ApproveTx", nil, string(jsarg))
|
||||
|
||||
if err != nil {
|
||||
log.Info("error occurred during execution", "error", err)
|
||||
return err
|
||||
}
|
||||
result, err := v.ToString()
|
||||
if err != nil {
|
||||
log.Info("error occurred during response unmarshalling", "error", err)
|
||||
return err
|
||||
|
||||
}
|
||||
if result == "Approve" {
|
||||
log.Info("Op approved")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("rejected")
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveTx(request *signer.SignTxRequest) (signer.SignTxResponse, error) {
|
||||
jsonreq, err := json.Marshal(request)
|
||||
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
|
||||
return signer.SignTxResponse{Transaction: request.Transaction, From: request.From, Approved: true, Password: ""}, nil
|
||||
}
|
||||
return signer.SignTxResponse{Approved: false}, err
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveSignData(request *signer.SignDataRequest) (signer.SignDataResponse, error) {
|
||||
jsonreq, err := json.Marshal(request)
|
||||
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
|
||||
return signer.SignDataResponse{Approved: true, Password: ""}, nil
|
||||
}
|
||||
return signer.SignDataResponse{Approved: false, Password: ""}, err
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveExport(request *signer.ExportRequest) (signer.ExportResponse, error) {
|
||||
jsonreq, err := json.Marshal(request)
|
||||
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
|
||||
return signer.ExportResponse{Approved: true}, nil
|
||||
}
|
||||
return signer.ExportResponse{Approved: false}, err
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveImport(request *signer.ImportRequest) (signer.ImportResponse, error) {
|
||||
// This cannot be handled by rules, requires setting a password
|
||||
// dispatch to next
|
||||
return r.next.ApproveImport(request)
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveListing(request *signer.ListRequest) (signer.ListResponse, error) {
|
||||
jsonreq, err := json.Marshal(request)
|
||||
if err = r.checkApproval("ApproveListing", jsonreq, err); err == nil {
|
||||
return signer.ListResponse{Accounts: request.Accounts}, nil
|
||||
}
|
||||
return signer.ListResponse{}, err
|
||||
}
|
||||
|
||||
func (r *rulesetUi) ApproveNewAccount(request *signer.NewAccountRequest) (signer.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)
|
||||
}
|
||||
316
cmd/signer/rules/rules_test.go
Normal file
316
cmd/signer/rules/rules_test.go
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/cmd/signer"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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 returnvalues, 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 hexAddr(a string) common.Address { return common.BytesToAddress(common.Hex2Bytes(a)) }
|
||||
func mixAddr(a string) (*common.MixedcaseAddress, error) {
|
||||
return common.NewMixedcaseAddressFromString(a)
|
||||
}
|
||||
|
||||
func initRuleEngine(js string) (*rulesetUi, error) {
|
||||
r, err := NewRuleEvaluator()
|
||||
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) {
|
||||
accs := make([]signer.Account, 5)
|
||||
|
||||
for i, _ := range accs {
|
||||
addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
|
||||
acc := signer.Account{
|
||||
Address: common.BytesToAddress(common.Hex2Bytes(addr)),
|
||||
URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
|
||||
}
|
||||
accs[i] = acc
|
||||
}
|
||||
|
||||
js := `function ApproveListing(accounts, meta){ return "Approve" }`
|
||||
|
||||
r, err := initRuleEngine(js)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't create evaluator %v", err)
|
||||
return
|
||||
}
|
||||
resp, err := r.ApproveListing(&signer.ListRequest{
|
||||
accs,
|
||||
signer.Metadata{
|
||||
"remoteip", "localip", "inproc",
|
||||
},
|
||||
})
|
||||
if len(resp.Accounts) != len(accs) {
|
||||
t.Errorf("Expected check to resolve to 'Approve'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignTxRequest(t *testing.T) {
|
||||
|
||||
js := `
|
||||
function ApproveTx(jsonstr){
|
||||
console.log(jsonstr)
|
||||
r = JSON.parse(jsonstr)
|
||||
console.log("from", r.from)
|
||||
console.log("transaction.to", r.transaction.to);
|
||||
console.log("transaction.value", r.transaction.value);
|
||||
console.log("transaction.nonce", r.transaction.nonce);
|
||||
if(r.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
|
||||
if(r.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
|
||||
}
|
||||
fmt.Printf("to %v", to.Address().String())
|
||||
resp, err := r.ApproveTx(&signer.SignTxRequest{
|
||||
Transaction: signer.TransactionArg{To: to},
|
||||
From: *from,
|
||||
Callinfo: "",
|
||||
Meta: signer.Metadata{"remoteip", "localip", "inproc"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
if !resp.Approved {
|
||||
t.Errorf("Expected check to resolve to 'Approve'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFunc(t *testing.T) {
|
||||
r, err := initRuleEngine(JS)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't create evaluator %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = r.vm.Call("MissingMethod", nil, "test")
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
|
||||
if r.checkApproval("MissingMethod", nil, nil) == nil {
|
||||
t.Errorf("Expected error to resolve to 'Reject'")
|
||||
}
|
||||
|
||||
fmt.Printf("Err %v", err)
|
||||
|
||||
}
|
||||
func TestStorage(t *testing.T) {
|
||||
|
||||
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.vm.Call("testStorage", nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
retval, err := v.ToString()
|
||||
|
||||
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)
|
||||
}
|
||||
fmt.Printf("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("Sum so far", sum);
|
||||
console.log("Requested", value.toNumber());
|
||||
// Would we exceed weekly limit ?
|
||||
if (sum.plus(value).lt(limit)){
|
||||
// Add this to the storage
|
||||
newtxs.push({tstamp: new Date().getTime(), value: value});
|
||||
storage.Put("txs", JSON.stringify(newtxs));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
function ApproveTx(jsonstr){
|
||||
r = JSON.parse(jsonstr);
|
||||
console.log("Requested value ", r.transaction.value)
|
||||
|
||||
if (isLimitOk(r.transaction)){
|
||||
return "Approve"
|
||||
}
|
||||
return "Nope"
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
func dummyTx(value *hexutil.Big) *signer.SignTxRequest {
|
||||
|
||||
to, _ := mixAddr("000000000000000000000000000000000000dead")
|
||||
from, _ := mixAddr("000000000000000000000000000000000000dead")
|
||||
|
||||
return &signer.SignTxRequest{
|
||||
Transaction: signer.TransactionArg{
|
||||
To: to,
|
||||
Value: value,
|
||||
},
|
||||
From: *from,
|
||||
Callinfo: "Warning, all your base are bellong to us",
|
||||
Meta: signer.Metadata{"remoteip", "localip", "inproc"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitWindow(t *testing.T) {
|
||||
|
||||
r, err := initRuleEngine(ExampleTxWindow)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't create evaluator %v", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
// 0.3 ether: 429D069189E0000 wei
|
||||
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
|
||||
h := hexutil.Big(*v)
|
||||
// The first three should succeed
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err := r.ApproveTx(dummyTx(&h))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
if !resp.Approved {
|
||||
t.Errorf("Expected check to resolve to 'Approve'")
|
||||
}
|
||||
}
|
||||
// Fourth should fail
|
||||
resp, err := r.ApproveTx(dummyTx(&h))
|
||||
if resp.Approved {
|
||||
t.Errorf("Expected check to resolve to 'Reject'")
|
||||
}
|
||||
|
||||
}
|
||||
76
cmd/signer/storage/storage.go
Normal file
76
cmd/signer/storage/storage.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
// Put stores a value by key. 0-length keys results in no-op
|
||||
Put(key, value string)
|
||||
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
|
||||
Get(key string) string
|
||||
// New creates a new (sub) namespace for the storage
|
||||
New(namespace string) Storage
|
||||
}
|
||||
|
||||
// EphemeralStorage is an in-memory storage that does
|
||||
// not persist values to disk. Mainly used for testing
|
||||
type EphemeralStorage struct {
|
||||
data map[string]string
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (s *EphemeralStorage) Put(key, value string) {
|
||||
if len(key) == 0 {
|
||||
return
|
||||
}
|
||||
key = fmt.Sprintf("%s.%s", s.namespace, key)
|
||||
fmt.Printf("storage: put %v -> %v\n", key, value)
|
||||
s.data[key] = value
|
||||
}
|
||||
|
||||
func (s *EphemeralStorage) Get(key string) string {
|
||||
|
||||
if len(key) == 0 {
|
||||
return ""
|
||||
}
|
||||
key = fmt.Sprintf("%s.%s", s.namespace, key)
|
||||
fmt.Printf("storage: get %v\n", key)
|
||||
if v, exist := s.data[key]; exist {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *EphemeralStorage) New(namespace string) Storage {
|
||||
child := &EphemeralStorage{
|
||||
data: make(map[string]string),
|
||||
namespace: fmt.Sprintf("%s.%s", namespace),
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
func NewEphemeralStorage() Storage {
|
||||
s := &EphemeralStorage{
|
||||
data: make(map[string]string),
|
||||
namespace: "root",
|
||||
}
|
||||
return s
|
||||
}
|
||||
Loading…
Reference in a new issue