copy from the following commit. commit 9b831d74fbee3d59ceca380a40e4a9af1a55cfca Author: Guillaume Ballet <gballet@gmail.com> Date: Tue May 7 18:22:24 2019 +0200 accounts/usbwallet: fix a comment typo in trezor driver (#19535) Signed-off-by: Leo Chen <leo@harmony.one>pull/832/head
parent
c120280e34
commit
d794c7bcc1
@ -0,0 +1,167 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
) |
||||
|
||||
// The ABI holds information about a contract's context and available
|
||||
// invokable methods. It will allow you to type check function calls and
|
||||
// packs data accordingly.
|
||||
type ABI struct { |
||||
Constructor Method |
||||
Methods map[string]Method |
||||
Events map[string]Event |
||||
} |
||||
|
||||
// JSON returns a parsed ABI interface and error if it failed.
|
||||
func JSON(reader io.Reader) (ABI, error) { |
||||
dec := json.NewDecoder(reader) |
||||
|
||||
var abi ABI |
||||
if err := dec.Decode(&abi); err != nil { |
||||
return ABI{}, err |
||||
} |
||||
|
||||
return abi, nil |
||||
} |
||||
|
||||
// Pack the given method name to conform the ABI. Method call's data
|
||||
// will consist of method_id, args0, arg1, ... argN. Method id consists
|
||||
// of 4 bytes and arguments are all 32 bytes.
|
||||
// Method ids are created from the first 4 bytes of the hash of the
|
||||
// methods string signature. (signature = baz(uint32,string32))
|
||||
func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { |
||||
// Fetch the ABI of the requested method
|
||||
if name == "" { |
||||
// constructor
|
||||
arguments, err := abi.Constructor.Inputs.Pack(args...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return arguments, nil |
||||
} |
||||
method, exist := abi.Methods[name] |
||||
if !exist { |
||||
return nil, fmt.Errorf("method '%s' not found", name) |
||||
} |
||||
arguments, err := method.Inputs.Pack(args...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Pack up the method ID too if not a constructor and return
|
||||
return append(method.Id(), arguments...), nil |
||||
} |
||||
|
||||
// Unpack output in v according to the abi specification
|
||||
func (abi ABI) Unpack(v interface{}, name string, data []byte) (err error) { |
||||
if len(data) == 0 { |
||||
return fmt.Errorf("abi: unmarshalling empty output") |
||||
} |
||||
// since there can't be naming collisions with contracts and events,
|
||||
// we need to decide whether we're calling a method or an event
|
||||
if method, ok := abi.Methods[name]; ok { |
||||
if len(data)%32 != 0 { |
||||
return fmt.Errorf("abi: improperly formatted output: %s - Bytes: [%+v]", string(data), data) |
||||
} |
||||
return method.Outputs.Unpack(v, data) |
||||
} |
||||
if event, ok := abi.Events[name]; ok { |
||||
return event.Inputs.Unpack(v, data) |
||||
} |
||||
return fmt.Errorf("abi: could not locate named method or event") |
||||
} |
||||
|
||||
// UnpackIntoMap unpacks a log into the provided map[string]interface{}
|
||||
func (abi ABI) UnpackIntoMap(v map[string]interface{}, name string, data []byte) (err error) { |
||||
if len(data) == 0 { |
||||
return fmt.Errorf("abi: unmarshalling empty output") |
||||
} |
||||
// since there can't be naming collisions with contracts and events,
|
||||
// we need to decide whether we're calling a method or an event
|
||||
if method, ok := abi.Methods[name]; ok { |
||||
if len(data)%32 != 0 { |
||||
return fmt.Errorf("abi: improperly formatted output") |
||||
} |
||||
return method.Outputs.UnpackIntoMap(v, data) |
||||
} |
||||
if event, ok := abi.Events[name]; ok { |
||||
return event.Inputs.UnpackIntoMap(v, data) |
||||
} |
||||
return fmt.Errorf("abi: could not locate named method or event") |
||||
} |
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface
|
||||
func (abi *ABI) UnmarshalJSON(data []byte) error { |
||||
var fields []struct { |
||||
Type string |
||||
Name string |
||||
Constant bool |
||||
Anonymous bool |
||||
Inputs []Argument |
||||
Outputs []Argument |
||||
} |
||||
|
||||
if err := json.Unmarshal(data, &fields); err != nil { |
||||
return err |
||||
} |
||||
|
||||
abi.Methods = make(map[string]Method) |
||||
abi.Events = make(map[string]Event) |
||||
for _, field := range fields { |
||||
switch field.Type { |
||||
case "constructor": |
||||
abi.Constructor = Method{ |
||||
Inputs: field.Inputs, |
||||
} |
||||
// empty defaults to function according to the abi spec
|
||||
case "function", "": |
||||
abi.Methods[field.Name] = Method{ |
||||
Name: field.Name, |
||||
Const: field.Constant, |
||||
Inputs: field.Inputs, |
||||
Outputs: field.Outputs, |
||||
} |
||||
case "event": |
||||
abi.Events[field.Name] = Event{ |
||||
Name: field.Name, |
||||
Anonymous: field.Anonymous, |
||||
Inputs: field.Inputs, |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// MethodById looks up a method by the 4-byte id
|
||||
// returns nil if none found
|
||||
func (abi *ABI) MethodById(sigdata []byte) (*Method, error) { |
||||
if len(sigdata) < 4 { |
||||
return nil, fmt.Errorf("data too short (%d bytes) for abi method lookup", len(sigdata)) |
||||
} |
||||
for _, method := range abi.Methods { |
||||
if bytes.Equal(method.Id(), sigdata[:4]) { |
||||
return &method, nil |
||||
} |
||||
} |
||||
return nil, fmt.Errorf("no method with id: %#x", sigdata[:4]) |
||||
} |
@ -0,0 +1,933 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/hex" |
||||
"fmt" |
||||
"log" |
||||
"math/big" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
const jsondata = ` |
||||
[ |
||||
{ "type" : "function", "name" : "balance", "constant" : true }, |
||||
{ "type" : "function", "name" : "send", "constant" : false, "inputs" : [ { "name" : "amount", "type" : "uint256" } ] } |
||||
]` |
||||
|
||||
const jsondata2 = ` |
||||
[ |
||||
{ "type" : "function", "name" : "balance", "constant" : true }, |
||||
{ "type" : "function", "name" : "send", "constant" : false, "inputs" : [ { "name" : "amount", "type" : "uint256" } ] }, |
||||
{ "type" : "function", "name" : "test", "constant" : false, "inputs" : [ { "name" : "number", "type" : "uint32" } ] }, |
||||
{ "type" : "function", "name" : "string", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "string" } ] }, |
||||
{ "type" : "function", "name" : "bool", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "bool" } ] }, |
||||
{ "type" : "function", "name" : "address", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "address" } ] }, |
||||
{ "type" : "function", "name" : "uint64[2]", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint64[2]" } ] }, |
||||
{ "type" : "function", "name" : "uint64[]", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint64[]" } ] }, |
||||
{ "type" : "function", "name" : "foo", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint32" } ] }, |
||||
{ "type" : "function", "name" : "bar", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint32" }, { "name" : "string", "type" : "uint16" } ] }, |
||||
{ "type" : "function", "name" : "slice", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint32[2]" } ] }, |
||||
{ "type" : "function", "name" : "slice256", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "uint256[2]" } ] }, |
||||
{ "type" : "function", "name" : "sliceAddress", "constant" : false, "inputs" : [ { "name" : "inputs", "type" : "address[]" } ] }, |
||||
{ "type" : "function", "name" : "sliceMultiAddress", "constant" : false, "inputs" : [ { "name" : "a", "type" : "address[]" }, { "name" : "b", "type" : "address[]" } ] }, |
||||
{ "type" : "function", "name" : "nestedArray", "constant" : false, "inputs" : [ { "name" : "a", "type" : "uint256[2][2]" }, { "name" : "b", "type" : "address[]" } ] }, |
||||
{ "type" : "function", "name" : "nestedArray2", "constant" : false, "inputs" : [ { "name" : "a", "type" : "uint8[][2]" } ] }, |
||||
{ "type" : "function", "name" : "nestedSlice", "constant" : false, "inputs" : [ { "name" : "a", "type" : "uint8[][]" } ] } |
||||
]` |
||||
|
||||
func TestReader(t *testing.T) { |
||||
Uint256, _ := NewType("uint256", nil) |
||||
exp := ABI{ |
||||
Methods: map[string]Method{ |
||||
"balance": { |
||||
"balance", true, nil, nil, |
||||
}, |
||||
"send": { |
||||
"send", false, []Argument{ |
||||
{"amount", Uint256, false}, |
||||
}, nil, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
abi, err := JSON(strings.NewReader(jsondata)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// deep equal fails for some reason
|
||||
for name, expM := range exp.Methods { |
||||
gotM, exist := abi.Methods[name] |
||||
if !exist { |
||||
t.Errorf("Missing expected method %v", name) |
||||
} |
||||
if !reflect.DeepEqual(gotM, expM) { |
||||
t.Errorf("\nGot abi method: \n%v\ndoes not match expected method\n%v", gotM, expM) |
||||
} |
||||
} |
||||
|
||||
for name, gotM := range abi.Methods { |
||||
expM, exist := exp.Methods[name] |
||||
if !exist { |
||||
t.Errorf("Found extra method %v", name) |
||||
} |
||||
if !reflect.DeepEqual(gotM, expM) { |
||||
t.Errorf("\nGot abi method: \n%v\ndoes not match expected method\n%v", gotM, expM) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestTestNumbers(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
if _, err := abi.Pack("balance"); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if _, err := abi.Pack("balance", 1); err == nil { |
||||
t.Error("expected error for balance(1)") |
||||
} |
||||
|
||||
if _, err := abi.Pack("doesntexist", nil); err == nil { |
||||
t.Errorf("doesntexist shouldn't exist") |
||||
} |
||||
|
||||
if _, err := abi.Pack("doesntexist", 1); err == nil { |
||||
t.Errorf("doesntexist(1) shouldn't exist") |
||||
} |
||||
|
||||
if _, err := abi.Pack("send", big.NewInt(1000)); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
i := new(int) |
||||
*i = 1000 |
||||
if _, err := abi.Pack("send", i); err == nil { |
||||
t.Errorf("expected send( ptr ) to throw, requires *big.Int instead of *int") |
||||
} |
||||
|
||||
if _, err := abi.Pack("test", uint32(1000)); err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func TestTestString(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
if _, err := abi.Pack("string", "hello world"); err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func TestTestBool(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
if _, err := abi.Pack("bool", true); err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func TestTestSlice(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
slice := make([]uint64, 2) |
||||
if _, err := abi.Pack("uint64[2]", slice); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if _, err := abi.Pack("uint64[]", slice); err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func TestMethodSignature(t *testing.T) { |
||||
String, _ := NewType("string", nil) |
||||
m := Method{"foo", false, []Argument{{"bar", String, false}, {"baz", String, false}}, nil} |
||||
exp := "foo(string,string)" |
||||
if m.Sig() != exp { |
||||
t.Error("signature mismatch", exp, "!=", m.Sig()) |
||||
} |
||||
|
||||
idexp := crypto.Keccak256([]byte(exp))[:4] |
||||
if !bytes.Equal(m.Id(), idexp) { |
||||
t.Errorf("expected ids to match %x != %x", m.Id(), idexp) |
||||
} |
||||
|
||||
uintt, _ := NewType("uint256", nil) |
||||
m = Method{"foo", false, []Argument{{"bar", uintt, false}}, nil} |
||||
exp = "foo(uint256)" |
||||
if m.Sig() != exp { |
||||
t.Error("signature mismatch", exp, "!=", m.Sig()) |
||||
} |
||||
|
||||
// Method with tuple arguments
|
||||
s, _ := NewType("tuple", []ArgumentMarshaling{ |
||||
{Name: "a", Type: "int256"}, |
||||
{Name: "b", Type: "int256[]"}, |
||||
{Name: "c", Type: "tuple[]", Components: []ArgumentMarshaling{ |
||||
{Name: "x", Type: "int256"}, |
||||
{Name: "y", Type: "int256"}, |
||||
}}, |
||||
{Name: "d", Type: "tuple[2]", Components: []ArgumentMarshaling{ |
||||
{Name: "x", Type: "int256"}, |
||||
{Name: "y", Type: "int256"}, |
||||
}}, |
||||
}) |
||||
m = Method{"foo", false, []Argument{{"s", s, false}, {"bar", String, false}}, nil} |
||||
exp = "foo((int256,int256[],(int256,int256)[],(int256,int256)[2]),string)" |
||||
if m.Sig() != exp { |
||||
t.Error("signature mismatch", exp, "!=", m.Sig()) |
||||
} |
||||
} |
||||
|
||||
func TestMultiPack(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
sig := crypto.Keccak256([]byte("bar(uint32,uint16)"))[:4] |
||||
sig = append(sig, make([]byte, 64)...) |
||||
sig[35] = 10 |
||||
sig[67] = 11 |
||||
|
||||
packed, err := abi.Pack("bar", uint32(10), uint16(11)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
t.FailNow() |
||||
} |
||||
|
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
} |
||||
|
||||
func ExampleJSON() { |
||||
const definition = `[{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isBar","outputs":[{"name":"","type":"bool"}],"type":"function"}]` |
||||
|
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
out, err := abi.Pack("isBar", common.HexToAddress("01")) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
fmt.Printf("%x\n", out) |
||||
// Output:
|
||||
// 1f2c40920000000000000000000000000000000000000000000000000000000000000001
|
||||
} |
||||
|
||||
func TestInputVariableInputLength(t *testing.T) { |
||||
const definition = `[ |
||||
{ "type" : "function", "name" : "strOne", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" } ] }, |
||||
{ "type" : "function", "name" : "bytesOne", "constant" : true, "inputs" : [ { "name" : "str", "type" : "bytes" } ] }, |
||||
{ "type" : "function", "name" : "strTwo", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" }, { "name" : "str1", "type" : "string" } ] } |
||||
]` |
||||
|
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// test one string
|
||||
strin := "hello world" |
||||
strpack, err := abi.Pack("strOne", strin) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
offset := make([]byte, 32) |
||||
offset[31] = 32 |
||||
length := make([]byte, 32) |
||||
length[31] = byte(len(strin)) |
||||
value := common.RightPadBytes([]byte(strin), 32) |
||||
exp := append(offset, append(length, value...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
strpack = strpack[4:] |
||||
if !bytes.Equal(strpack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, strpack) |
||||
} |
||||
|
||||
// test one bytes
|
||||
btspack, err := abi.Pack("bytesOne", []byte(strin)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
btspack = btspack[4:] |
||||
if !bytes.Equal(btspack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, btspack) |
||||
} |
||||
|
||||
// test two strings
|
||||
str1 := "hello" |
||||
str2 := "world" |
||||
str2pack, err := abi.Pack("strTwo", str1, str2) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
offset1 := make([]byte, 32) |
||||
offset1[31] = 64 |
||||
length1 := make([]byte, 32) |
||||
length1[31] = byte(len(str1)) |
||||
value1 := common.RightPadBytes([]byte(str1), 32) |
||||
|
||||
offset2 := make([]byte, 32) |
||||
offset2[31] = 128 |
||||
length2 := make([]byte, 32) |
||||
length2[31] = byte(len(str2)) |
||||
value2 := common.RightPadBytes([]byte(str2), 32) |
||||
|
||||
exp2 := append(offset1, offset2...) |
||||
exp2 = append(exp2, append(length1, value1...)...) |
||||
exp2 = append(exp2, append(length2, value2...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
str2pack = str2pack[4:] |
||||
if !bytes.Equal(str2pack, exp2) { |
||||
t.Errorf("expected %x, got %x\n", exp, str2pack) |
||||
} |
||||
|
||||
// test two strings, first > 32, second < 32
|
||||
str1 = strings.Repeat("a", 33) |
||||
str2pack, err = abi.Pack("strTwo", str1, str2) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
offset1 = make([]byte, 32) |
||||
offset1[31] = 64 |
||||
length1 = make([]byte, 32) |
||||
length1[31] = byte(len(str1)) |
||||
value1 = common.RightPadBytes([]byte(str1), 64) |
||||
offset2[31] = 160 |
||||
|
||||
exp2 = append(offset1, offset2...) |
||||
exp2 = append(exp2, append(length1, value1...)...) |
||||
exp2 = append(exp2, append(length2, value2...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
str2pack = str2pack[4:] |
||||
if !bytes.Equal(str2pack, exp2) { |
||||
t.Errorf("expected %x, got %x\n", exp, str2pack) |
||||
} |
||||
|
||||
// test two strings, first > 32, second >32
|
||||
str1 = strings.Repeat("a", 33) |
||||
str2 = strings.Repeat("a", 33) |
||||
str2pack, err = abi.Pack("strTwo", str1, str2) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
offset1 = make([]byte, 32) |
||||
offset1[31] = 64 |
||||
length1 = make([]byte, 32) |
||||
length1[31] = byte(len(str1)) |
||||
value1 = common.RightPadBytes([]byte(str1), 64) |
||||
|
||||
offset2 = make([]byte, 32) |
||||
offset2[31] = 160 |
||||
length2 = make([]byte, 32) |
||||
length2[31] = byte(len(str2)) |
||||
value2 = common.RightPadBytes([]byte(str2), 64) |
||||
|
||||
exp2 = append(offset1, offset2...) |
||||
exp2 = append(exp2, append(length1, value1...)...) |
||||
exp2 = append(exp2, append(length2, value2...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
str2pack = str2pack[4:] |
||||
if !bytes.Equal(str2pack, exp2) { |
||||
t.Errorf("expected %x, got %x\n", exp, str2pack) |
||||
} |
||||
} |
||||
|
||||
func TestInputFixedArrayAndVariableInputLength(t *testing.T) { |
||||
const definition = `[ |
||||
{ "type" : "function", "name" : "fixedArrStr", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" }, { "name" : "fixedArr", "type" : "uint256[2]" } ] }, |
||||
{ "type" : "function", "name" : "fixedArrBytes", "constant" : true, "inputs" : [ { "name" : "str", "type" : "bytes" }, { "name" : "fixedArr", "type" : "uint256[2]" } ] }, |
||||
{ "type" : "function", "name" : "mixedArrStr", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" }, { "name" : "fixedArr", "type": "uint256[2]" }, { "name" : "dynArr", "type": "uint256[]" } ] }, |
||||
{ "type" : "function", "name" : "doubleFixedArrStr", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" }, { "name" : "fixedArr1", "type": "uint256[2]" }, { "name" : "fixedArr2", "type": "uint256[3]" } ] }, |
||||
{ "type" : "function", "name" : "multipleMixedArrStr", "constant" : true, "inputs" : [ { "name" : "str", "type" : "string" }, { "name" : "fixedArr1", "type": "uint256[2]" }, { "name" : "dynArr", "type" : "uint256[]" }, { "name" : "fixedArr2", "type" : "uint256[3]" } ] } |
||||
]` |
||||
|
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// test string, fixed array uint256[2]
|
||||
strin := "hello world" |
||||
arrin := [2]*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
fixedArrStrPack, err := abi.Pack("fixedArrStr", strin, arrin) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// generate expected output
|
||||
offset := make([]byte, 32) |
||||
offset[31] = 96 |
||||
length := make([]byte, 32) |
||||
length[31] = byte(len(strin)) |
||||
strvalue := common.RightPadBytes([]byte(strin), 32) |
||||
arrinvalue1 := common.LeftPadBytes(arrin[0].Bytes(), 32) |
||||
arrinvalue2 := common.LeftPadBytes(arrin[1].Bytes(), 32) |
||||
exp := append(offset, arrinvalue1...) |
||||
exp = append(exp, arrinvalue2...) |
||||
exp = append(exp, append(length, strvalue...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
fixedArrStrPack = fixedArrStrPack[4:] |
||||
if !bytes.Equal(fixedArrStrPack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, fixedArrStrPack) |
||||
} |
||||
|
||||
// test byte array, fixed array uint256[2]
|
||||
bytesin := []byte(strin) |
||||
arrin = [2]*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
fixedArrBytesPack, err := abi.Pack("fixedArrBytes", bytesin, arrin) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// generate expected output
|
||||
offset = make([]byte, 32) |
||||
offset[31] = 96 |
||||
length = make([]byte, 32) |
||||
length[31] = byte(len(strin)) |
||||
strvalue = common.RightPadBytes([]byte(strin), 32) |
||||
arrinvalue1 = common.LeftPadBytes(arrin[0].Bytes(), 32) |
||||
arrinvalue2 = common.LeftPadBytes(arrin[1].Bytes(), 32) |
||||
exp = append(offset, arrinvalue1...) |
||||
exp = append(exp, arrinvalue2...) |
||||
exp = append(exp, append(length, strvalue...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
fixedArrBytesPack = fixedArrBytesPack[4:] |
||||
if !bytes.Equal(fixedArrBytesPack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, fixedArrBytesPack) |
||||
} |
||||
|
||||
// test string, fixed array uint256[2], dynamic array uint256[]
|
||||
strin = "hello world" |
||||
fixedarrin := [2]*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
dynarrin := []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)} |
||||
mixedArrStrPack, err := abi.Pack("mixedArrStr", strin, fixedarrin, dynarrin) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// generate expected output
|
||||
stroffset := make([]byte, 32) |
||||
stroffset[31] = 128 |
||||
strlength := make([]byte, 32) |
||||
strlength[31] = byte(len(strin)) |
||||
strvalue = common.RightPadBytes([]byte(strin), 32) |
||||
fixedarrinvalue1 := common.LeftPadBytes(fixedarrin[0].Bytes(), 32) |
||||
fixedarrinvalue2 := common.LeftPadBytes(fixedarrin[1].Bytes(), 32) |
||||
dynarroffset := make([]byte, 32) |
||||
dynarroffset[31] = byte(160 + ((len(strin)/32)+1)*32) |
||||
dynarrlength := make([]byte, 32) |
||||
dynarrlength[31] = byte(len(dynarrin)) |
||||
dynarrinvalue1 := common.LeftPadBytes(dynarrin[0].Bytes(), 32) |
||||
dynarrinvalue2 := common.LeftPadBytes(dynarrin[1].Bytes(), 32) |
||||
dynarrinvalue3 := common.LeftPadBytes(dynarrin[2].Bytes(), 32) |
||||
exp = append(stroffset, fixedarrinvalue1...) |
||||
exp = append(exp, fixedarrinvalue2...) |
||||
exp = append(exp, dynarroffset...) |
||||
exp = append(exp, append(strlength, strvalue...)...) |
||||
dynarrarg := append(dynarrlength, dynarrinvalue1...) |
||||
dynarrarg = append(dynarrarg, dynarrinvalue2...) |
||||
dynarrarg = append(dynarrarg, dynarrinvalue3...) |
||||
exp = append(exp, dynarrarg...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
mixedArrStrPack = mixedArrStrPack[4:] |
||||
if !bytes.Equal(mixedArrStrPack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, mixedArrStrPack) |
||||
} |
||||
|
||||
// test string, fixed array uint256[2], fixed array uint256[3]
|
||||
strin = "hello world" |
||||
fixedarrin1 := [2]*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
fixedarrin2 := [3]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)} |
||||
doubleFixedArrStrPack, err := abi.Pack("doubleFixedArrStr", strin, fixedarrin1, fixedarrin2) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// generate expected output
|
||||
stroffset = make([]byte, 32) |
||||
stroffset[31] = 192 |
||||
strlength = make([]byte, 32) |
||||
strlength[31] = byte(len(strin)) |
||||
strvalue = common.RightPadBytes([]byte(strin), 32) |
||||
fixedarrin1value1 := common.LeftPadBytes(fixedarrin1[0].Bytes(), 32) |
||||
fixedarrin1value2 := common.LeftPadBytes(fixedarrin1[1].Bytes(), 32) |
||||
fixedarrin2value1 := common.LeftPadBytes(fixedarrin2[0].Bytes(), 32) |
||||
fixedarrin2value2 := common.LeftPadBytes(fixedarrin2[1].Bytes(), 32) |
||||
fixedarrin2value3 := common.LeftPadBytes(fixedarrin2[2].Bytes(), 32) |
||||
exp = append(stroffset, fixedarrin1value1...) |
||||
exp = append(exp, fixedarrin1value2...) |
||||
exp = append(exp, fixedarrin2value1...) |
||||
exp = append(exp, fixedarrin2value2...) |
||||
exp = append(exp, fixedarrin2value3...) |
||||
exp = append(exp, append(strlength, strvalue...)...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
doubleFixedArrStrPack = doubleFixedArrStrPack[4:] |
||||
if !bytes.Equal(doubleFixedArrStrPack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, doubleFixedArrStrPack) |
||||
} |
||||
|
||||
// test string, fixed array uint256[2], dynamic array uint256[], fixed array uint256[3]
|
||||
strin = "hello world" |
||||
fixedarrin1 = [2]*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
dynarrin = []*big.Int{big.NewInt(1), big.NewInt(2)} |
||||
fixedarrin2 = [3]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)} |
||||
multipleMixedArrStrPack, err := abi.Pack("multipleMixedArrStr", strin, fixedarrin1, dynarrin, fixedarrin2) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
// generate expected output
|
||||
stroffset = make([]byte, 32) |
||||
stroffset[31] = 224 |
||||
strlength = make([]byte, 32) |
||||
strlength[31] = byte(len(strin)) |
||||
strvalue = common.RightPadBytes([]byte(strin), 32) |
||||
fixedarrin1value1 = common.LeftPadBytes(fixedarrin1[0].Bytes(), 32) |
||||
fixedarrin1value2 = common.LeftPadBytes(fixedarrin1[1].Bytes(), 32) |
||||
dynarroffset = U256(big.NewInt(int64(256 + ((len(strin)/32)+1)*32))) |
||||
dynarrlength = make([]byte, 32) |
||||
dynarrlength[31] = byte(len(dynarrin)) |
||||
dynarrinvalue1 = common.LeftPadBytes(dynarrin[0].Bytes(), 32) |
||||
dynarrinvalue2 = common.LeftPadBytes(dynarrin[1].Bytes(), 32) |
||||
fixedarrin2value1 = common.LeftPadBytes(fixedarrin2[0].Bytes(), 32) |
||||
fixedarrin2value2 = common.LeftPadBytes(fixedarrin2[1].Bytes(), 32) |
||||
fixedarrin2value3 = common.LeftPadBytes(fixedarrin2[2].Bytes(), 32) |
||||
exp = append(stroffset, fixedarrin1value1...) |
||||
exp = append(exp, fixedarrin1value2...) |
||||
exp = append(exp, dynarroffset...) |
||||
exp = append(exp, fixedarrin2value1...) |
||||
exp = append(exp, fixedarrin2value2...) |
||||
exp = append(exp, fixedarrin2value3...) |
||||
exp = append(exp, append(strlength, strvalue...)...) |
||||
dynarrarg = append(dynarrlength, dynarrinvalue1...) |
||||
dynarrarg = append(dynarrarg, dynarrinvalue2...) |
||||
exp = append(exp, dynarrarg...) |
||||
|
||||
// ignore first 4 bytes of the output. This is the function identifier
|
||||
multipleMixedArrStrPack = multipleMixedArrStrPack[4:] |
||||
if !bytes.Equal(multipleMixedArrStrPack, exp) { |
||||
t.Errorf("expected %x, got %x\n", exp, multipleMixedArrStrPack) |
||||
} |
||||
} |
||||
|
||||
func TestDefaultFunctionParsing(t *testing.T) { |
||||
const definition = `[{ "name" : "balance" }]` |
||||
|
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, ok := abi.Methods["balance"]; !ok { |
||||
t.Error("expected 'balance' to be present") |
||||
} |
||||
} |
||||
|
||||
func TestBareEvents(t *testing.T) { |
||||
const definition = `[ |
||||
{ "type" : "event", "name" : "balance" }, |
||||
{ "type" : "event", "name" : "anon", "anonymous" : true}, |
||||
{ "type" : "event", "name" : "args", "inputs" : [{ "indexed":false, "name":"arg0", "type":"uint256" }, { "indexed":true, "name":"arg1", "type":"address" }] }, |
||||
{ "type" : "event", "name" : "tuple", "inputs" : [{ "indexed":false, "name":"t", "type":"tuple", "components":[{"name":"a", "type":"uint256"}] }, { "indexed":true, "name":"arg1", "type":"address" }] } |
||||
]` |
||||
|
||||
arg0, _ := NewType("uint256", nil) |
||||
arg1, _ := NewType("address", nil) |
||||
tuple, _ := NewType("tuple", []ArgumentMarshaling{{Name: "a", Type: "uint256"}}) |
||||
|
||||
expectedEvents := map[string]struct { |
||||
Anonymous bool |
||||
Args []Argument |
||||
}{ |
||||
"balance": {false, nil}, |
||||
"anon": {true, nil}, |
||||
"args": {false, []Argument{ |
||||
{Name: "arg0", Type: arg0, Indexed: false}, |
||||
{Name: "arg1", Type: arg1, Indexed: true}, |
||||
}}, |
||||
"tuple": {false, []Argument{ |
||||
{Name: "t", Type: tuple, Indexed: false}, |
||||
{Name: "arg1", Type: arg1, Indexed: true}, |
||||
}}, |
||||
} |
||||
|
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(abi.Events) != len(expectedEvents) { |
||||
t.Fatalf("invalid number of events after parsing, want %d, got %d", len(expectedEvents), len(abi.Events)) |
||||
} |
||||
|
||||
for name, exp := range expectedEvents { |
||||
got, ok := abi.Events[name] |
||||
if !ok { |
||||
t.Errorf("could not found event %s", name) |
||||
continue |
||||
} |
||||
if got.Anonymous != exp.Anonymous { |
||||
t.Errorf("invalid anonymous indication for event %s, want %v, got %v", name, exp.Anonymous, got.Anonymous) |
||||
} |
||||
if len(got.Inputs) != len(exp.Args) { |
||||
t.Errorf("invalid number of args, want %d, got %d", len(exp.Args), len(got.Inputs)) |
||||
continue |
||||
} |
||||
for i, arg := range exp.Args { |
||||
if arg.Name != got.Inputs[i].Name { |
||||
t.Errorf("events[%s].Input[%d] has an invalid name, want %s, got %s", name, i, arg.Name, got.Inputs[i].Name) |
||||
} |
||||
if arg.Indexed != got.Inputs[i].Indexed { |
||||
t.Errorf("events[%s].Input[%d] has an invalid indexed indication, want %v, got %v", name, i, arg.Indexed, got.Inputs[i].Indexed) |
||||
} |
||||
if arg.Type.T != got.Inputs[i].Type.T { |
||||
t.Errorf("events[%s].Input[%d] has an invalid type, want %x, got %x", name, i, arg.Type.T, got.Inputs[i].Type.T) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TestUnpackEvent is based on this contract:
|
||||
// contract T {
|
||||
// event received(address sender, uint amount, bytes memo);
|
||||
// event receivedAddr(address sender);
|
||||
// function receive(bytes memo) external payable {
|
||||
// received(msg.sender, msg.value, memo);
|
||||
// receivedAddr(msg.sender);
|
||||
// }
|
||||
// }
|
||||
// When receive("X") is called with sender 0x00... and value 1, it produces this tx receipt:
|
||||
// receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]}
|
||||
func TestUnpackEvent(t *testing.T) { |
||||
const abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` |
||||
abi, err := JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
const hexdata = `000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158` |
||||
data, err := hex.DecodeString(hexdata) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
|
||||
type ReceivedEvent struct { |
||||
Sender common.Address |
||||
Amount *big.Int |
||||
Memo []byte |
||||
} |
||||
var ev ReceivedEvent |
||||
|
||||
err = abi.Unpack(&ev, "received", data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
type ReceivedAddrEvent struct { |
||||
Sender common.Address |
||||
} |
||||
var receivedAddrEv ReceivedAddrEvent |
||||
err = abi.Unpack(&receivedAddrEv, "receivedAddr", data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func TestUnpackEventIntoMap(t *testing.T) { |
||||
const abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` |
||||
abi, err := JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
const hexdata = `000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158` |
||||
data, err := hex.DecodeString(hexdata) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
|
||||
receivedMap := map[string]interface{}{} |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := abi.UnpackIntoMap(receivedMap, "received", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(receivedMap) != 3 { |
||||
t.Error("unpacked `received` map expected to have length 3") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
|
||||
receivedAddrMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(receivedAddrMap, "receivedAddr", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(receivedAddrMap) != 1 { |
||||
t.Error("unpacked `receivedAddr` map expected to have length 1") |
||||
} |
||||
if receivedAddrMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked `receivedAddr` map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackMethodIntoMap(t *testing.T) { |
||||
const abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[],"name":"send","outputs":[{"name":"amount","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"addr","type":"address"}],"name":"get","outputs":[{"name":"hash","type":"bytes"}],"payable":true,"stateMutability":"payable","type":"function"}]` |
||||
abi, err := JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
const hexdata = `00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000015800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000158000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001580000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000015800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000158` |
||||
data, err := hex.DecodeString(hexdata) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 != 0 { |
||||
t.Errorf("len(data) is %d, want a multiple of 32", len(data)) |
||||
} |
||||
|
||||
// Tests a method with no outputs
|
||||
receiveMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(receiveMap, "receive", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(receiveMap) > 0 { |
||||
t.Error("unpacked `receive` map expected to have length 0") |
||||
} |
||||
|
||||
// Tests a method with only outputs
|
||||
sendMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(sendMap, "send", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(sendMap) != 1 { |
||||
t.Error("unpacked `send` map expected to have length 1") |
||||
} |
||||
if sendMap["amount"].(*big.Int).Cmp(big.NewInt(1)) != 0 { |
||||
t.Error("unpacked `send` map expected `amount` value of 1") |
||||
} |
||||
|
||||
// Tests a method with outputs and inputs
|
||||
getMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(getMap, "get", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(sendMap) != 1 { |
||||
t.Error("unpacked `get` map expected to have length 1") |
||||
} |
||||
expectedBytes := []byte{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, 96, 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, 1, 88, 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, 96, 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, 1, 88, 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, 96, 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, 1, 88, 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, 96, 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, 1, 88, 0} |
||||
if !bytes.Equal(getMap["hash"].([]byte), expectedBytes) { |
||||
t.Errorf("unpacked `get` map expected `hash` value of %v", expectedBytes) |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIntoMapNamingConflict(t *testing.T) { |
||||
// Two methods have the same name
|
||||
var abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"get","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[],"name":"send","outputs":[{"name":"amount","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"addr","type":"address"}],"name":"get","outputs":[{"name":"hash","type":"bytes"}],"payable":true,"stateMutability":"payable","type":"function"}]` |
||||
abi, err := JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
var hexdata = `00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158` |
||||
data, err := hex.DecodeString(hexdata) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
getMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(getMap, "get", data); err == nil { |
||||
t.Error("naming conflict between two methods; error expected") |
||||
} |
||||
|
||||
// Two events have the same name
|
||||
abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"received","type":"event"}]` |
||||
abi, err = JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
hexdata = `000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158` |
||||
data, err = hex.DecodeString(hexdata) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
receivedMap := map[string]interface{}{} |
||||
if err = abi.UnpackIntoMap(receivedMap, "received", data); err != nil { |
||||
t.Error("naming conflict between two events; no error expected") |
||||
} |
||||
if len(receivedMap) != 1 { |
||||
t.Error("naming conflict between two events; event defined latest in the abi expected to be used") |
||||
} |
||||
|
||||
// Method and event have the same name
|
||||
abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"received","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` |
||||
abi, err = JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
if err = abi.UnpackIntoMap(receivedMap, "received", data); err == nil { |
||||
t.Error("naming conflict between an event and a method; error expected") |
||||
} |
||||
|
||||
// Conflict is case sensitive
|
||||
abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"received","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"Received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` |
||||
abi, err = JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(data)%32 == 0 { |
||||
t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) |
||||
} |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err = abi.UnpackIntoMap(receivedMap, "Received", data); err != nil { |
||||
t.Error(err) |
||||
} |
||||
if len(receivedMap) != 3 { |
||||
t.Error("unpacked `received` map expected to have length 3") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked `received` map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestABI_MethodById(t *testing.T) { |
||||
const abiJSON = `[ |
||||
{"type":"function","name":"receive","constant":false,"inputs":[{"name":"memo","type":"bytes"}],"outputs":[],"payable":true,"stateMutability":"payable"}, |
||||
{"type":"event","name":"received","anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}]}, |
||||
{"type":"function","name":"fixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr","type":"uint256[2]"}]}, |
||||
{"type":"function","name":"fixedArrBytes","constant":true,"inputs":[{"name":"str","type":"bytes"},{"name":"fixedArr","type":"uint256[2]"}]}, |
||||
{"type":"function","name":"mixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr","type":"uint256[2]"},{"name":"dynArr","type":"uint256[]"}]}, |
||||
{"type":"function","name":"doubleFixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr1","type":"uint256[2]"},{"name":"fixedArr2","type":"uint256[3]"}]}, |
||||
{"type":"function","name":"multipleMixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr1","type":"uint256[2]"},{"name":"dynArr","type":"uint256[]"},{"name":"fixedArr2","type":"uint256[3]"}]}, |
||||
{"type":"function","name":"balance","constant":true}, |
||||
{"type":"function","name":"send","constant":false,"inputs":[{"name":"amount","type":"uint256"}]}, |
||||
{"type":"function","name":"test","constant":false,"inputs":[{"name":"number","type":"uint32"}]}, |
||||
{"type":"function","name":"string","constant":false,"inputs":[{"name":"inputs","type":"string"}]}, |
||||
{"type":"function","name":"bool","constant":false,"inputs":[{"name":"inputs","type":"bool"}]}, |
||||
{"type":"function","name":"address","constant":false,"inputs":[{"name":"inputs","type":"address"}]}, |
||||
{"type":"function","name":"uint64[2]","constant":false,"inputs":[{"name":"inputs","type":"uint64[2]"}]}, |
||||
{"type":"function","name":"uint64[]","constant":false,"inputs":[{"name":"inputs","type":"uint64[]"}]}, |
||||
{"type":"function","name":"foo","constant":false,"inputs":[{"name":"inputs","type":"uint32"}]}, |
||||
{"type":"function","name":"bar","constant":false,"inputs":[{"name":"inputs","type":"uint32"},{"name":"string","type":"uint16"}]}, |
||||
{"type":"function","name":"_slice","constant":false,"inputs":[{"name":"inputs","type":"uint32[2]"}]}, |
||||
{"type":"function","name":"__slice256","constant":false,"inputs":[{"name":"inputs","type":"uint256[2]"}]}, |
||||
{"type":"function","name":"sliceAddress","constant":false,"inputs":[{"name":"inputs","type":"address[]"}]}, |
||||
{"type":"function","name":"sliceMultiAddress","constant":false,"inputs":[{"name":"a","type":"address[]"},{"name":"b","type":"address[]"}]} |
||||
] |
||||
` |
||||
abi, err := JSON(strings.NewReader(abiJSON)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
for name, m := range abi.Methods { |
||||
a := fmt.Sprintf("%v", m) |
||||
m2, err := abi.MethodById(m.Id()) |
||||
if err != nil { |
||||
t.Fatalf("Failed to look up ABI method: %v", err) |
||||
} |
||||
b := fmt.Sprintf("%v", m2) |
||||
if a != b { |
||||
t.Errorf("Method %v (id %v) not 'findable' by id in ABI", name, common.ToHex(m.Id())) |
||||
} |
||||
} |
||||
// Also test empty
|
||||
if _, err := abi.MethodById([]byte{0x00}); err == nil { |
||||
t.Errorf("Expected error, too short to decode data") |
||||
} |
||||
if _, err := abi.MethodById([]byte{}); err == nil { |
||||
t.Errorf("Expected error, too short to decode data") |
||||
} |
||||
if _, err := abi.MethodById(nil); err == nil { |
||||
t.Errorf("Expected error, nil is short to decode data") |
||||
} |
||||
} |
@ -0,0 +1,354 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"reflect" |
||||
"strings" |
||||
) |
||||
|
||||
// Argument holds the name of the argument and the corresponding type.
|
||||
// Types are used when packing and testing arguments.
|
||||
type Argument struct { |
||||
Name string |
||||
Type Type |
||||
Indexed bool // indexed is only used by events
|
||||
} |
||||
|
||||
type Arguments []Argument |
||||
|
||||
type ArgumentMarshaling struct { |
||||
Name string |
||||
Type string |
||||
Components []ArgumentMarshaling |
||||
Indexed bool |
||||
} |
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface
|
||||
func (argument *Argument) UnmarshalJSON(data []byte) error { |
||||
var arg ArgumentMarshaling |
||||
err := json.Unmarshal(data, &arg) |
||||
if err != nil { |
||||
return fmt.Errorf("argument json err: %v", err) |
||||
} |
||||
|
||||
argument.Type, err = NewType(arg.Type, arg.Components) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
argument.Name = arg.Name |
||||
argument.Indexed = arg.Indexed |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// LengthNonIndexed returns the number of arguments when not counting 'indexed' ones. Only events
|
||||
// can ever have 'indexed' arguments, it should always be false on arguments for method input/output
|
||||
func (arguments Arguments) LengthNonIndexed() int { |
||||
out := 0 |
||||
for _, arg := range arguments { |
||||
if !arg.Indexed { |
||||
out++ |
||||
} |
||||
} |
||||
return out |
||||
} |
||||
|
||||
// NonIndexed returns the arguments with indexed arguments filtered out
|
||||
func (arguments Arguments) NonIndexed() Arguments { |
||||
var ret []Argument |
||||
for _, arg := range arguments { |
||||
if !arg.Indexed { |
||||
ret = append(ret, arg) |
||||
} |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// isTuple returns true for non-atomic constructs, like (uint,uint) or uint[]
|
||||
func (arguments Arguments) isTuple() bool { |
||||
return len(arguments) > 1 |
||||
} |
||||
|
||||
// Unpack performs the operation hexdata -> Go format
|
||||
func (arguments Arguments) Unpack(v interface{}, data []byte) error { |
||||
// make sure the passed value is arguments pointer
|
||||
if reflect.Ptr != reflect.ValueOf(v).Kind() { |
||||
return fmt.Errorf("abi: Unpack(non-pointer %T)", v) |
||||
} |
||||
marshalledValues, err := arguments.UnpackValues(data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if arguments.isTuple() { |
||||
return arguments.unpackTuple(v, marshalledValues) |
||||
} |
||||
return arguments.unpackAtomic(v, marshalledValues[0]) |
||||
} |
||||
|
||||
// UnpackIntoMap performs the operation hexdata -> mapping of argument name to argument value
|
||||
func (arguments Arguments) UnpackIntoMap(v map[string]interface{}, data []byte) error { |
||||
marshalledValues, err := arguments.UnpackValues(data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return arguments.unpackIntoMap(v, marshalledValues) |
||||
} |
||||
|
||||
// unpack sets the unmarshalled value to go format.
|
||||
// Note the dst here must be settable.
|
||||
func unpack(t *Type, dst interface{}, src interface{}) error { |
||||
var ( |
||||
dstVal = reflect.ValueOf(dst).Elem() |
||||
srcVal = reflect.ValueOf(src) |
||||
) |
||||
|
||||
if t.T != TupleTy && !((t.T == SliceTy || t.T == ArrayTy) && t.Elem.T == TupleTy) { |
||||
return set(dstVal, srcVal) |
||||
} |
||||
|
||||
switch t.T { |
||||
case TupleTy: |
||||
if dstVal.Kind() != reflect.Struct { |
||||
return fmt.Errorf("abi: invalid dst value for unpack, want struct, got %s", dstVal.Kind()) |
||||
} |
||||
fieldmap, err := mapArgNamesToStructFields(t.TupleRawNames, dstVal) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for i, elem := range t.TupleElems { |
||||
fname := fieldmap[t.TupleRawNames[i]] |
||||
field := dstVal.FieldByName(fname) |
||||
if !field.IsValid() { |
||||
return fmt.Errorf("abi: field %s can't found in the given value", t.TupleRawNames[i]) |
||||
} |
||||
if err := unpack(elem, field.Addr().Interface(), srcVal.Field(i).Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
case SliceTy: |
||||
if dstVal.Kind() != reflect.Slice { |
||||
return fmt.Errorf("abi: invalid dst value for unpack, want slice, got %s", dstVal.Kind()) |
||||
} |
||||
slice := reflect.MakeSlice(dstVal.Type(), srcVal.Len(), srcVal.Len()) |
||||
for i := 0; i < slice.Len(); i++ { |
||||
if err := unpack(t.Elem, slice.Index(i).Addr().Interface(), srcVal.Index(i).Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
dstVal.Set(slice) |
||||
case ArrayTy: |
||||
if dstVal.Kind() != reflect.Array { |
||||
return fmt.Errorf("abi: invalid dst value for unpack, want array, got %s", dstVal.Kind()) |
||||
} |
||||
array := reflect.New(dstVal.Type()).Elem() |
||||
for i := 0; i < array.Len(); i++ { |
||||
if err := unpack(t.Elem, array.Index(i).Addr().Interface(), srcVal.Index(i).Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
dstVal.Set(array) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// unpackIntoMap unpacks marshalledValues into the provided map[string]interface{}
|
||||
func (arguments Arguments) unpackIntoMap(v map[string]interface{}, marshalledValues []interface{}) error { |
||||
// Make sure map is not nil
|
||||
if v == nil { |
||||
return fmt.Errorf("abi: cannot unpack into a nil map") |
||||
} |
||||
|
||||
for i, arg := range arguments.NonIndexed() { |
||||
v[arg.Name] = marshalledValues[i] |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// unpackAtomic unpacks ( hexdata -> go ) a single value
|
||||
func (arguments Arguments) unpackAtomic(v interface{}, marshalledValues interface{}) error { |
||||
if arguments.LengthNonIndexed() == 0 { |
||||
return nil |
||||
} |
||||
argument := arguments.NonIndexed()[0] |
||||
elem := reflect.ValueOf(v).Elem() |
||||
|
||||
if elem.Kind() == reflect.Struct { |
||||
fieldmap, err := mapArgNamesToStructFields([]string{argument.Name}, elem) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
field := elem.FieldByName(fieldmap[argument.Name]) |
||||
if !field.IsValid() { |
||||
return fmt.Errorf("abi: field %s can't be found in the given value", argument.Name) |
||||
} |
||||
return unpack(&argument.Type, field.Addr().Interface(), marshalledValues) |
||||
} |
||||
return unpack(&argument.Type, elem.Addr().Interface(), marshalledValues) |
||||
} |
||||
|
||||
// unpackTuple unpacks ( hexdata -> go ) a batch of values.
|
||||
func (arguments Arguments) unpackTuple(v interface{}, marshalledValues []interface{}) error { |
||||
var ( |
||||
value = reflect.ValueOf(v).Elem() |
||||
typ = value.Type() |
||||
kind = value.Kind() |
||||
) |
||||
if err := requireUnpackKind(value, typ, kind, arguments); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// If the interface is a struct, get of abi->struct_field mapping
|
||||
var abi2struct map[string]string |
||||
if kind == reflect.Struct { |
||||
var ( |
||||
argNames []string |
||||
err error |
||||
) |
||||
for _, arg := range arguments.NonIndexed() { |
||||
argNames = append(argNames, arg.Name) |
||||
} |
||||
abi2struct, err = mapArgNamesToStructFields(argNames, value) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
for i, arg := range arguments.NonIndexed() { |
||||
switch kind { |
||||
case reflect.Struct: |
||||
field := value.FieldByName(abi2struct[arg.Name]) |
||||
if !field.IsValid() { |
||||
return fmt.Errorf("abi: field %s can't be found in the given value", arg.Name) |
||||
} |
||||
if err := unpack(&arg.Type, field.Addr().Interface(), marshalledValues[i]); err != nil { |
||||
return err |
||||
} |
||||
case reflect.Slice, reflect.Array: |
||||
if value.Len() < i { |
||||
return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(arguments), value.Len()) |
||||
} |
||||
v := value.Index(i) |
||||
if err := requireAssignable(v, reflect.ValueOf(marshalledValues[i])); err != nil { |
||||
return err |
||||
} |
||||
if err := unpack(&arg.Type, v.Addr().Interface(), marshalledValues[i]); err != nil { |
||||
return err |
||||
} |
||||
default: |
||||
return fmt.Errorf("abi:[2] cannot unmarshal tuple in to %v", typ) |
||||
} |
||||
} |
||||
return nil |
||||
|
||||
} |
||||
|
||||
// UnpackValues can be used to unpack ABI-encoded hexdata according to the ABI-specification,
|
||||
// without supplying a struct to unpack into. Instead, this method returns a list containing the
|
||||
// values. An atomic argument will be a list with one element.
|
||||
func (arguments Arguments) UnpackValues(data []byte) ([]interface{}, error) { |
||||
retval := make([]interface{}, 0, arguments.LengthNonIndexed()) |
||||
virtualArgs := 0 |
||||
for index, arg := range arguments.NonIndexed() { |
||||
marshalledValue, err := toGoType((index+virtualArgs)*32, arg.Type, data) |
||||
if arg.Type.T == ArrayTy && !isDynamicType(arg.Type) { |
||||
// If we have a static array, like [3]uint256, these are coded as
|
||||
// just like uint256,uint256,uint256.
|
||||
// This means that we need to add two 'virtual' arguments when
|
||||
// we count the index from now on.
|
||||
//
|
||||
// Array values nested multiple levels deep are also encoded inline:
|
||||
// [2][3]uint256: uint256,uint256,uint256,uint256,uint256,uint256
|
||||
//
|
||||
// Calculate the full array size to get the correct offset for the next argument.
|
||||
// Decrement it by 1, as the normal index increment is still applied.
|
||||
virtualArgs += getTypeSize(arg.Type)/32 - 1 |
||||
} else if arg.Type.T == TupleTy && !isDynamicType(arg.Type) { |
||||
// If we have a static tuple, like (uint256, bool, uint256), these are
|
||||
// coded as just like uint256,bool,uint256
|
||||
virtualArgs += getTypeSize(arg.Type)/32 - 1 |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
retval = append(retval, marshalledValue) |
||||
} |
||||
return retval, nil |
||||
} |
||||
|
||||
// PackValues performs the operation Go format -> Hexdata
|
||||
// It is the semantic opposite of UnpackValues
|
||||
func (arguments Arguments) PackValues(args []interface{}) ([]byte, error) { |
||||
return arguments.Pack(args...) |
||||
} |
||||
|
||||
// Pack performs the operation Go format -> Hexdata
|
||||
func (arguments Arguments) Pack(args ...interface{}) ([]byte, error) { |
||||
// Make sure arguments match up and pack them
|
||||
abiArgs := arguments |
||||
if len(args) != len(abiArgs) { |
||||
return nil, fmt.Errorf("argument count mismatch: %d for %d", len(args), len(abiArgs)) |
||||
} |
||||
// variable input is the output appended at the end of packed
|
||||
// output. This is used for strings and bytes types input.
|
||||
var variableInput []byte |
||||
|
||||
// input offset is the bytes offset for packed output
|
||||
inputOffset := 0 |
||||
for _, abiArg := range abiArgs { |
||||
inputOffset += getTypeSize(abiArg.Type) |
||||
} |
||||
var ret []byte |
||||
for i, a := range args { |
||||
input := abiArgs[i] |
||||
// pack the input
|
||||
packed, err := input.Type.pack(reflect.ValueOf(a)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// check for dynamic types
|
||||
if isDynamicType(input.Type) { |
||||
// set the offset
|
||||
ret = append(ret, packNum(reflect.ValueOf(inputOffset))...) |
||||
// calculate next offset
|
||||
inputOffset += len(packed) |
||||
// append to variable input
|
||||
variableInput = append(variableInput, packed...) |
||||
} else { |
||||
// append the packed value to the input
|
||||
ret = append(ret, packed...) |
||||
} |
||||
} |
||||
// append the variable input at the end of the packed input
|
||||
ret = append(ret, variableInput...) |
||||
|
||||
return ret, nil |
||||
} |
||||
|
||||
// ToCamelCase converts an under-score string to a camel-case string
|
||||
func ToCamelCase(input string) string { |
||||
parts := strings.Split(input, "_") |
||||
for i, s := range parts { |
||||
if len(s) > 0 { |
||||
parts[i] = strings.ToUpper(s[:1]) + s[1:] |
||||
} |
||||
} |
||||
return strings.Join(parts, "") |
||||
} |
@ -0,0 +1,62 @@ |
||||
// Copyright 2016 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 bind |
||||
|
||||
import ( |
||||
"crypto/ecdsa" |
||||
"errors" |
||||
"io" |
||||
"io/ioutil" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// NewTransactor is a utility method to easily create a transaction signer from
|
||||
// an encrypted json key stream and the associated passphrase.
|
||||
func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) { |
||||
json, err := ioutil.ReadAll(keyin) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
key, err := keystore.DecryptKey(json, passphrase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return NewKeyedTransactor(key.PrivateKey), nil |
||||
} |
||||
|
||||
// NewKeyedTransactor is a utility method to easily create a transaction signer
|
||||
// from a single private key.
|
||||
func NewKeyedTransactor(key *ecdsa.PrivateKey) *TransactOpts { |
||||
keyAddr := crypto.PubkeyToAddress(key.PublicKey) |
||||
return &TransactOpts{ |
||||
From: keyAddr, |
||||
Signer: func(signer types.Signer, address common.Address, tx *types.Transaction) (*types.Transaction, error) { |
||||
if address != keyAddr { |
||||
return nil, errors.New("not authorized to sign this account") |
||||
} |
||||
signature, err := crypto.Sign(signer.Hash(tx).Bytes(), key) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return tx.WithSignature(signer, signature) |
||||
}, |
||||
} |
||||
} |
@ -0,0 +1,112 @@ |
||||
// Copyright 2015 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 bind |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
) |
||||
|
||||
var ( |
||||
// ErrNoCode is returned by call and transact operations for which the requested
|
||||
// recipient contract to operate on does not exist in the state db or does not
|
||||
// have any code associated with it (i.e. suicided).
|
||||
ErrNoCode = errors.New("no contract code at given address") |
||||
|
||||
// This error is raised when attempting to perform a pending state action
|
||||
// on a backend that doesn't implement PendingContractCaller.
|
||||
ErrNoPendingState = errors.New("backend does not support pending state") |
||||
|
||||
// This error is returned by WaitDeployed if contract creation leaves an
|
||||
// empty contract behind.
|
||||
ErrNoCodeAfterDeploy = errors.New("no contract code after deployment") |
||||
) |
||||
|
||||
// ContractCaller defines the methods needed to allow operating with contract on a read
|
||||
// only basis.
|
||||
type ContractCaller interface { |
||||
// CodeAt returns the code of the given account. This is needed to differentiate
|
||||
// between contract internal errors and the local chain being out of sync.
|
||||
CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) |
||||
// ContractCall executes an Ethereum contract call with the specified data as the
|
||||
// input.
|
||||
CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) |
||||
} |
||||
|
||||
// PendingContractCaller defines methods to perform contract calls on the pending state.
|
||||
// Call will try to discover this interface when access to the pending state is requested.
|
||||
// If the backend does not support the pending state, Call returns ErrNoPendingState.
|
||||
type PendingContractCaller interface { |
||||
// PendingCodeAt returns the code of the given account in the pending state.
|
||||
PendingCodeAt(ctx context.Context, contract common.Address) ([]byte, error) |
||||
// PendingCallContract executes an Ethereum contract call against the pending state.
|
||||
PendingCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) |
||||
} |
||||
|
||||
// ContractTransactor defines the methods needed to allow operating with contract
|
||||
// on a write only basis. Beside the transacting method, the remainder are helpers
|
||||
// used when the user does not provide some needed values, but rather leaves it up
|
||||
// to the transactor to decide.
|
||||
type ContractTransactor interface { |
||||
// PendingCodeAt returns the code of the given account in the pending state.
|
||||
PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) |
||||
// PendingNonceAt retrieves the current pending nonce associated with an account.
|
||||
PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) |
||||
// SuggestGasPrice retrieves the currently suggested gas price to allow a timely
|
||||
// execution of a transaction.
|
||||
SuggestGasPrice(ctx context.Context) (*big.Int, error) |
||||
// EstimateGas tries to estimate the gas needed to execute a specific
|
||||
// transaction based on the current pending state of the backend blockchain.
|
||||
// There is no guarantee that this is the true gas limit requirement as other
|
||||
// transactions may be added or removed by miners, but it should provide a basis
|
||||
// for setting a reasonable default.
|
||||
EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) |
||||
// SendTransaction injects the transaction into the pending pool for execution.
|
||||
SendTransaction(ctx context.Context, tx *types.Transaction) error |
||||
} |
||||
|
||||
// ContractFilterer defines the methods needed to access log events using one-off
|
||||
// queries or continuous event subscriptions.
|
||||
type ContractFilterer interface { |
||||
// FilterLogs executes a log filter operation, blocking during execution and
|
||||
// returning all the results in one batch.
|
||||
//
|
||||
// TODO(karalabe): Deprecate when the subscription one can return past data too.
|
||||
FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) |
||||
|
||||
// SubscribeFilterLogs creates a background log filtering operation, returning
|
||||
// a subscription immediately, which can be used to stream the found events.
|
||||
SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) |
||||
} |
||||
|
||||
// DeployBackend wraps the operations needed by WaitMined and WaitDeployed.
|
||||
type DeployBackend interface { |
||||
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) |
||||
CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) |
||||
} |
||||
|
||||
// ContractBackend defines the methods needed to work with contracts on a read-write basis.
|
||||
type ContractBackend interface { |
||||
ContractCaller |
||||
ContractTransactor |
||||
ContractFilterer |
||||
} |
@ -0,0 +1,505 @@ |
||||
// Copyright 2015 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 backends |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
"github.com/ethereum/go-ethereum/consensus/ethash" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/core/bloombits" |
||||
"github.com/ethereum/go-ethereum/core/rawdb" |
||||
"github.com/ethereum/go-ethereum/core/state" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/core/vm" |
||||
"github.com/ethereum/go-ethereum/eth/filters" |
||||
"github.com/ethereum/go-ethereum/ethdb" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/ethereum/go-ethereum/rpc" |
||||
) |
||||
|
||||
// This nil assignment ensures compile time that SimulatedBackend implements bind.ContractBackend.
|
||||
var _ bind.ContractBackend = (*SimulatedBackend)(nil) |
||||
|
||||
var errBlockNumberUnsupported = errors.New("SimulatedBackend cannot access blocks other than the latest block") |
||||
var errGasEstimationFailed = errors.New("gas required exceeds allowance or always failing transaction") |
||||
|
||||
// SimulatedBackend implements bind.ContractBackend, simulating a blockchain in
|
||||
// the background. Its main purpose is to allow easily testing contract bindings.
|
||||
type SimulatedBackend struct { |
||||
database ethdb.Database // In memory database to store our testing data
|
||||
blockchain *core.BlockChain // Ethereum blockchain to handle the consensus
|
||||
|
||||
mu sync.Mutex |
||||
pendingBlock *types.Block // Currently pending block that will be imported on request
|
||||
pendingState *state.StateDB // Currently pending state that will be the active on on request
|
||||
|
||||
events *filters.EventSystem // Event system for filtering log events live
|
||||
|
||||
config *params.ChainConfig |
||||
} |
||||
|
||||
// NewSimulatedBackend creates a new binding backend using a simulated blockchain
|
||||
// for testing purposes.
|
||||
func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend { |
||||
database := rawdb.NewMemoryDatabase() |
||||
genesis := core.Genesis{Config: params.AllEthashProtocolChanges, GasLimit: gasLimit, Alloc: alloc} |
||||
genesis.MustCommit(database) |
||||
blockchain, _ := core.NewBlockChain(database, nil, genesis.Config, ethash.NewFaker(), vm.Config{}, nil) |
||||
|
||||
backend := &SimulatedBackend{ |
||||
database: database, |
||||
blockchain: blockchain, |
||||
config: genesis.Config, |
||||
events: filters.NewEventSystem(new(event.TypeMux), &filterBackend{database, blockchain}, false), |
||||
} |
||||
backend.rollback() |
||||
return backend |
||||
} |
||||
|
||||
// Commit imports all the pending transactions as a single block and starts a
|
||||
// fresh new state.
|
||||
func (b *SimulatedBackend) Commit() { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if _, err := b.blockchain.InsertChain([]*types.Block{b.pendingBlock}); err != nil { |
||||
panic(err) // This cannot happen unless the simulator is wrong, fail in that case
|
||||
} |
||||
b.rollback() |
||||
} |
||||
|
||||
// Rollback aborts all pending transactions, reverting to the last committed state.
|
||||
func (b *SimulatedBackend) Rollback() { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
b.rollback() |
||||
} |
||||
|
||||
func (b *SimulatedBackend) rollback() { |
||||
blocks, _ := core.GenerateChain(b.config, b.blockchain.CurrentBlock(), ethash.NewFaker(), b.database, 1, func(int, *core.BlockGen) {}) |
||||
statedb, _ := b.blockchain.State() |
||||
|
||||
b.pendingBlock = blocks[0] |
||||
b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) |
||||
} |
||||
|
||||
// CodeAt returns the code associated with a certain account in the blockchain.
|
||||
func (b *SimulatedBackend) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number()) != 0 { |
||||
return nil, errBlockNumberUnsupported |
||||
} |
||||
statedb, _ := b.blockchain.State() |
||||
return statedb.GetCode(contract), nil |
||||
} |
||||
|
||||
// BalanceAt returns the wei balance of a certain account in the blockchain.
|
||||
func (b *SimulatedBackend) BalanceAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (*big.Int, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number()) != 0 { |
||||
return nil, errBlockNumberUnsupported |
||||
} |
||||
statedb, _ := b.blockchain.State() |
||||
return statedb.GetBalance(contract), nil |
||||
} |
||||
|
||||
// NonceAt returns the nonce of a certain account in the blockchain.
|
||||
func (b *SimulatedBackend) NonceAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (uint64, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number()) != 0 { |
||||
return 0, errBlockNumberUnsupported |
||||
} |
||||
statedb, _ := b.blockchain.State() |
||||
return statedb.GetNonce(contract), nil |
||||
} |
||||
|
||||
// StorageAt returns the value of key in the storage of an account in the blockchain.
|
||||
func (b *SimulatedBackend) StorageAt(ctx context.Context, contract common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number()) != 0 { |
||||
return nil, errBlockNumberUnsupported |
||||
} |
||||
statedb, _ := b.blockchain.State() |
||||
val := statedb.GetState(contract, key) |
||||
return val[:], nil |
||||
} |
||||
|
||||
// TransactionReceipt returns the receipt of a transaction.
|
||||
func (b *SimulatedBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { |
||||
receipt, _, _, _ := rawdb.ReadReceipt(b.database, txHash, b.config) |
||||
return receipt, nil |
||||
} |
||||
|
||||
// TransactionByHash checks the pool of pending transactions in addition to the
|
||||
// blockchain. The isPending return value indicates whether the transaction has been
|
||||
// mined yet. Note that the transaction may not be part of the canonical chain even if
|
||||
// it's not pending.
|
||||
func (b *SimulatedBackend) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
tx := b.pendingBlock.Transaction(txHash) |
||||
if tx != nil { |
||||
return tx, true, nil |
||||
} |
||||
tx, _, _, _ = rawdb.ReadTransaction(b.database, txHash) |
||||
if tx != nil { |
||||
return tx, false, nil |
||||
} |
||||
return nil, false, ethereum.NotFound |
||||
} |
||||
|
||||
// PendingCodeAt returns the code associated with an account in the pending state.
|
||||
func (b *SimulatedBackend) PendingCodeAt(ctx context.Context, contract common.Address) ([]byte, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
return b.pendingState.GetCode(contract), nil |
||||
} |
||||
|
||||
// CallContract executes a contract call.
|
||||
func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number()) != 0 { |
||||
return nil, errBlockNumberUnsupported |
||||
} |
||||
state, err := b.blockchain.State() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rval, _, _, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), state) |
||||
return rval, err |
||||
} |
||||
|
||||
// PendingCallContract executes a contract call on the pending state.
|
||||
func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
defer b.pendingState.RevertToSnapshot(b.pendingState.Snapshot()) |
||||
|
||||
rval, _, _, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) |
||||
return rval, err |
||||
} |
||||
|
||||
// PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving
|
||||
// the nonce currently pending for the account.
|
||||
func (b *SimulatedBackend) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
return b.pendingState.GetOrNewStateObject(account).Nonce(), nil |
||||
} |
||||
|
||||
// SuggestGasPrice implements ContractTransactor.SuggestGasPrice. Since the simulated
|
||||
// chain doesn't have miners, we just return a gas price of 1 for any call.
|
||||
func (b *SimulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) { |
||||
return big.NewInt(1), nil |
||||
} |
||||
|
||||
// EstimateGas executes the requested code against the currently pending block/state and
|
||||
// returns the used amount of gas.
|
||||
func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
// Determine the lowest and highest possible gas limits to binary search in between
|
||||
var ( |
||||
lo uint64 = params.TxGas - 1 |
||||
hi uint64 |
||||
cap uint64 |
||||
) |
||||
if call.Gas >= params.TxGas { |
||||
hi = call.Gas |
||||
} else { |
||||
hi = b.pendingBlock.GasLimit() |
||||
} |
||||
cap = hi |
||||
|
||||
// Create a helper to check if a gas allowance results in an executable transaction
|
||||
executable := func(gas uint64) bool { |
||||
call.Gas = gas |
||||
|
||||
snapshot := b.pendingState.Snapshot() |
||||
_, _, failed, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) |
||||
b.pendingState.RevertToSnapshot(snapshot) |
||||
|
||||
if err != nil || failed { |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
// Execute the binary search and hone in on an executable gas limit
|
||||
for lo+1 < hi { |
||||
mid := (hi + lo) / 2 |
||||
if !executable(mid) { |
||||
lo = mid |
||||
} else { |
||||
hi = mid |
||||
} |
||||
} |
||||
// Reject the transaction as invalid if it still fails at the highest allowance
|
||||
if hi == cap { |
||||
if !executable(hi) { |
||||
return 0, errGasEstimationFailed |
||||
} |
||||
} |
||||
return hi, nil |
||||
} |
||||
|
||||
// callContract implements common code between normal and pending contract calls.
|
||||
// state is modified during execution, make sure to copy it if necessary.
|
||||
func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallMsg, block *types.Block, statedb *state.StateDB) ([]byte, uint64, bool, error) { |
||||
// Ensure message is initialized properly.
|
||||
if call.GasPrice == nil { |
||||
call.GasPrice = big.NewInt(1) |
||||
} |
||||
if call.Gas == 0 { |
||||
call.Gas = 50000000 |
||||
} |
||||
if call.Value == nil { |
||||
call.Value = new(big.Int) |
||||
} |
||||
// Set infinite balance to the fake caller account.
|
||||
from := statedb.GetOrNewStateObject(call.From) |
||||
from.SetBalance(math.MaxBig256) |
||||
// Execute the call.
|
||||
msg := callmsg{call} |
||||
|
||||
evmContext := core.NewEVMContext(msg, block.Header(), b.blockchain, nil) |
||||
// Create a new environment which holds all relevant information
|
||||
// about the transaction and calling mechanisms.
|
||||
vmenv := vm.NewEVM(evmContext, statedb, b.config, vm.Config{}) |
||||
gaspool := new(core.GasPool).AddGas(math.MaxUint64) |
||||
|
||||
return core.NewStateTransition(vmenv, msg, gaspool).TransitionDb() |
||||
} |
||||
|
||||
// SendTransaction updates the pending block to include the given transaction.
|
||||
// It panics if the transaction is invalid.
|
||||
func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
|
||||
sender, err := types.Sender(types.HomesteadSigner{}, tx) |
||||
if err != nil { |
||||
panic(fmt.Errorf("invalid transaction: %v", err)) |
||||
} |
||||
nonce := b.pendingState.GetNonce(sender) |
||||
if tx.Nonce() != nonce { |
||||
panic(fmt.Errorf("invalid transaction nonce: got %d, want %d", tx.Nonce(), nonce)) |
||||
} |
||||
|
||||
blocks, _ := core.GenerateChain(b.config, b.blockchain.CurrentBlock(), ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) { |
||||
for _, tx := range b.pendingBlock.Transactions() { |
||||
block.AddTxWithChain(b.blockchain, tx) |
||||
} |
||||
block.AddTxWithChain(b.blockchain, tx) |
||||
}) |
||||
statedb, _ := b.blockchain.State() |
||||
|
||||
b.pendingBlock = blocks[0] |
||||
b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) |
||||
return nil |
||||
} |
||||
|
||||
// FilterLogs executes a log filter operation, blocking during execution and
|
||||
// returning all the results in one batch.
|
||||
//
|
||||
// TODO(karalabe): Deprecate when the subscription one can return past data too.
|
||||
func (b *SimulatedBackend) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { |
||||
var filter *filters.Filter |
||||
if query.BlockHash != nil { |
||||
// Block filter requested, construct a single-shot filter
|
||||
filter = filters.NewBlockFilter(&filterBackend{b.database, b.blockchain}, *query.BlockHash, query.Addresses, query.Topics) |
||||
} else { |
||||
// Initialize unset filter boundaried to run from genesis to chain head
|
||||
from := int64(0) |
||||
if query.FromBlock != nil { |
||||
from = query.FromBlock.Int64() |
||||
} |
||||
to := int64(-1) |
||||
if query.ToBlock != nil { |
||||
to = query.ToBlock.Int64() |
||||
} |
||||
// Construct the range filter
|
||||
filter = filters.NewRangeFilter(&filterBackend{b.database, b.blockchain}, from, to, query.Addresses, query.Topics) |
||||
} |
||||
// Run the filter and return all the logs
|
||||
logs, err := filter.Logs(ctx) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
res := make([]types.Log, len(logs)) |
||||
for i, log := range logs { |
||||
res[i] = *log |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
// SubscribeFilterLogs creates a background log filtering operation, returning a
|
||||
// subscription immediately, which can be used to stream the found events.
|
||||
func (b *SimulatedBackend) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { |
||||
// Subscribe to contract events
|
||||
sink := make(chan []*types.Log) |
||||
|
||||
sub, err := b.events.SubscribeLogs(query, sink) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Since we're getting logs in batches, we need to flatten them into a plain stream
|
||||
return event.NewSubscription(func(quit <-chan struct{}) error { |
||||
defer sub.Unsubscribe() |
||||
for { |
||||
select { |
||||
case logs := <-sink: |
||||
for _, log := range logs { |
||||
select { |
||||
case ch <- *log: |
||||
case err := <-sub.Err(): |
||||
return err |
||||
case <-quit: |
||||
return nil |
||||
} |
||||
} |
||||
case err := <-sub.Err(): |
||||
return err |
||||
case <-quit: |
||||
return nil |
||||
} |
||||
} |
||||
}), nil |
||||
} |
||||
|
||||
// AdjustTime adds a time shift to the simulated clock.
|
||||
func (b *SimulatedBackend) AdjustTime(adjustment time.Duration) error { |
||||
b.mu.Lock() |
||||
defer b.mu.Unlock() |
||||
blocks, _ := core.GenerateChain(b.config, b.blockchain.CurrentBlock(), ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) { |
||||
for _, tx := range b.pendingBlock.Transactions() { |
||||
block.AddTx(tx) |
||||
} |
||||
block.OffsetTime(int64(adjustment.Seconds())) |
||||
}) |
||||
statedb, _ := b.blockchain.State() |
||||
|
||||
b.pendingBlock = blocks[0] |
||||
b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// callmsg implements core.Message to allow passing it as a transaction simulator.
|
||||
type callmsg struct { |
||||
ethereum.CallMsg |
||||
} |
||||
|
||||
func (m callmsg) From() common.Address { return m.CallMsg.From } |
||||
func (m callmsg) Nonce() uint64 { return 0 } |
||||
func (m callmsg) CheckNonce() bool { return false } |
||||
func (m callmsg) To() *common.Address { return m.CallMsg.To } |
||||
func (m callmsg) GasPrice() *big.Int { return m.CallMsg.GasPrice } |
||||
func (m callmsg) Gas() uint64 { return m.CallMsg.Gas } |
||||
func (m callmsg) Value() *big.Int { return m.CallMsg.Value } |
||||
func (m callmsg) Data() []byte { return m.CallMsg.Data } |
||||
|
||||
// filterBackend implements filters.Backend to support filtering for logs without
|
||||
// taking bloom-bits acceleration structures into account.
|
||||
type filterBackend struct { |
||||
db ethdb.Database |
||||
bc *core.BlockChain |
||||
} |
||||
|
||||
func (fb *filterBackend) ChainDb() ethdb.Database { return fb.db } |
||||
func (fb *filterBackend) EventMux() *event.TypeMux { panic("not supported") } |
||||
|
||||
func (fb *filterBackend) HeaderByNumber(ctx context.Context, block rpc.BlockNumber) (*types.Header, error) { |
||||
if block == rpc.LatestBlockNumber { |
||||
return fb.bc.CurrentHeader(), nil |
||||
} |
||||
return fb.bc.GetHeaderByNumber(uint64(block.Int64())), nil |
||||
} |
||||
|
||||
func (fb *filterBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { |
||||
return fb.bc.GetHeaderByHash(hash), nil |
||||
} |
||||
|
||||
func (fb *filterBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { |
||||
number := rawdb.ReadHeaderNumber(fb.db, hash) |
||||
if number == nil { |
||||
return nil, nil |
||||
} |
||||
return rawdb.ReadReceipts(fb.db, hash, *number, fb.bc.Config()), nil |
||||
} |
||||
|
||||
func (fb *filterBackend) GetLogs(ctx context.Context, hash common.Hash) ([][]*types.Log, error) { |
||||
number := rawdb.ReadHeaderNumber(fb.db, hash) |
||||
if number == nil { |
||||
return nil, nil |
||||
} |
||||
receipts := rawdb.ReadReceipts(fb.db, hash, *number, fb.bc.Config()) |
||||
if receipts == nil { |
||||
return nil, nil |
||||
} |
||||
logs := make([][]*types.Log, len(receipts)) |
||||
for i, receipt := range receipts { |
||||
logs[i] = receipt.Logs |
||||
} |
||||
return logs, nil |
||||
} |
||||
|
||||
func (fb *filterBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription { |
||||
return event.NewSubscription(func(quit <-chan struct{}) error { |
||||
<-quit |
||||
return nil |
||||
}) |
||||
} |
||||
func (fb *filterBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { |
||||
return fb.bc.SubscribeChainEvent(ch) |
||||
} |
||||
func (fb *filterBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { |
||||
return fb.bc.SubscribeRemovedLogsEvent(ch) |
||||
} |
||||
func (fb *filterBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { |
||||
return fb.bc.SubscribeLogsEvent(ch) |
||||
} |
||||
|
||||
func (fb *filterBackend) BloomStatus() (uint64, uint64) { return 4096, 0 } |
||||
func (fb *filterBackend) ServiceFilter(ctx context.Context, ms *bloombits.MatcherSession) { |
||||
panic("not supported") |
||||
} |
@ -0,0 +1,82 @@ |
||||
// 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 backends_test |
||||
|
||||
import ( |
||||
"context" |
||||
"math/big" |
||||
"testing" |
||||
|
||||
ethereum "github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
func TestSimulatedBackend(t *testing.T) { |
||||
var gasLimit uint64 = 8000029 |
||||
key, _ := crypto.GenerateKey() // nolint: gosec
|
||||
auth := bind.NewKeyedTransactor(key) |
||||
genAlloc := make(core.GenesisAlloc) |
||||
genAlloc[auth.From] = core.GenesisAccount{Balance: big.NewInt(9223372036854775807)} |
||||
|
||||
sim := backends.NewSimulatedBackend(genAlloc, gasLimit) |
||||
|
||||
// should return an error if the tx is not found
|
||||
txHash := common.HexToHash("2") |
||||
_, isPending, err := sim.TransactionByHash(context.Background(), txHash) |
||||
|
||||
if isPending { |
||||
t.Fatal("transaction should not be pending") |
||||
} |
||||
if err != ethereum.NotFound { |
||||
t.Fatalf("err should be `ethereum.NotFound` but received %v", err) |
||||
} |
||||
|
||||
// generate a transaction and confirm you can retrieve it
|
||||
code := `6060604052600a8060106000396000f360606040526008565b00` |
||||
var gas uint64 = 3000000 |
||||
tx := types.NewContractCreation(0, big.NewInt(0), gas, big.NewInt(1), common.FromHex(code)) |
||||
tx, _ = types.SignTx(tx, types.HomesteadSigner{}, key) |
||||
|
||||
err = sim.SendTransaction(context.Background(), tx) |
||||
if err != nil { |
||||
t.Fatal("error sending transaction") |
||||
} |
||||
|
||||
txHash = tx.Hash() |
||||
_, isPending, err = sim.TransactionByHash(context.Background(), txHash) |
||||
if err != nil { |
||||
t.Fatalf("error getting transaction with hash: %v", txHash.String()) |
||||
} |
||||
if !isPending { |
||||
t.Fatal("transaction should have pending status") |
||||
} |
||||
|
||||
sim.Commit() |
||||
tx, isPending, err = sim.TransactionByHash(context.Background(), txHash) |
||||
if err != nil { |
||||
t.Fatalf("error getting transaction with hash: %v", txHash.String()) |
||||
} |
||||
if isPending { |
||||
t.Fatal("transaction should not have pending status") |
||||
} |
||||
|
||||
} |
@ -0,0 +1,366 @@ |
||||
// Copyright 2015 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 bind |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
) |
||||
|
||||
// SignerFn is a signer function callback when a contract requires a method to
|
||||
// sign the transaction before submission.
|
||||
type SignerFn func(types.Signer, common.Address, *types.Transaction) (*types.Transaction, error) |
||||
|
||||
// CallOpts is the collection of options to fine tune a contract call request.
|
||||
type CallOpts struct { |
||||
Pending bool // Whether to operate on the pending state or the last known one
|
||||
From common.Address // Optional the sender address, otherwise the first account is used
|
||||
BlockNumber *big.Int // Optional the block number on which the call should be performed
|
||||
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
|
||||
} |
||||
|
||||
// TransactOpts is the collection of authorization data required to create a
|
||||
// valid Ethereum transaction.
|
||||
type TransactOpts struct { |
||||
From common.Address // Ethereum account to send the transaction from
|
||||
Nonce *big.Int // Nonce to use for the transaction execution (nil = use pending state)
|
||||
Signer SignerFn // Method to use for signing the transaction (mandatory)
|
||||
|
||||
Value *big.Int // Funds to transfer along along the transaction (nil = 0 = no funds)
|
||||
GasPrice *big.Int // Gas price to use for the transaction execution (nil = gas price oracle)
|
||||
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)
|
||||
|
||||
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
|
||||
} |
||||
|
||||
// FilterOpts is the collection of options to fine tune filtering for events
|
||||
// within a bound contract.
|
||||
type FilterOpts struct { |
||||
Start uint64 // Start of the queried range
|
||||
End *uint64 // End of the range (nil = latest)
|
||||
|
||||
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
|
||||
} |
||||
|
||||
// WatchOpts is the collection of options to fine tune subscribing for events
|
||||
// within a bound contract.
|
||||
type WatchOpts struct { |
||||
Start *uint64 // Start of the queried range (nil = latest)
|
||||
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
|
||||
} |
||||
|
||||
// BoundContract is the base wrapper object that reflects a contract on the
|
||||
// Ethereum network. It contains a collection of methods that are used by the
|
||||
// higher level contract bindings to operate.
|
||||
type BoundContract struct { |
||||
address common.Address // Deployment address of the contract on the Ethereum blockchain
|
||||
abi abi.ABI // Reflect based ABI to access the correct Ethereum methods
|
||||
caller ContractCaller // Read interface to interact with the blockchain
|
||||
transactor ContractTransactor // Write interface to interact with the blockchain
|
||||
filterer ContractFilterer // Event filtering to interact with the blockchain
|
||||
} |
||||
|
||||
// NewBoundContract creates a low level contract interface through which calls
|
||||
// and transactions may be made through.
|
||||
func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, filterer ContractFilterer) *BoundContract { |
||||
return &BoundContract{ |
||||
address: address, |
||||
abi: abi, |
||||
caller: caller, |
||||
transactor: transactor, |
||||
filterer: filterer, |
||||
} |
||||
} |
||||
|
||||
// DeployContract deploys a contract onto the Ethereum blockchain and binds the
|
||||
// deployment address with a Go wrapper.
|
||||
func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, params ...interface{}) (common.Address, *types.Transaction, *BoundContract, error) { |
||||
// Otherwise try to deploy the contract
|
||||
c := NewBoundContract(common.Address{}, abi, backend, backend, backend) |
||||
|
||||
input, err := c.abi.Pack("", params...) |
||||
if err != nil { |
||||
return common.Address{}, nil, nil, err |
||||
} |
||||
tx, err := c.transact(opts, nil, append(bytecode, input...)) |
||||
if err != nil { |
||||
return common.Address{}, nil, nil, err |
||||
} |
||||
c.address = crypto.CreateAddress(opts.From, tx.Nonce()) |
||||
return c.address, tx, c, nil |
||||
} |
||||
|
||||
// Call invokes the (constant) contract method with params as input values and
|
||||
// sets the output to result. The result type might be a single field for simple
|
||||
// returns, a slice of interfaces for anonymous returns and a struct for named
|
||||
// returns.
|
||||
func (c *BoundContract) Call(opts *CallOpts, result interface{}, method string, params ...interface{}) error { |
||||
// Don't crash on a lazy user
|
||||
if opts == nil { |
||||
opts = new(CallOpts) |
||||
} |
||||
// Pack the input, call and unpack the results
|
||||
input, err := c.abi.Pack(method, params...) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var ( |
||||
msg = ethereum.CallMsg{From: opts.From, To: &c.address, Data: input} |
||||
ctx = ensureContext(opts.Context) |
||||
code []byte |
||||
output []byte |
||||
) |
||||
if opts.Pending { |
||||
pb, ok := c.caller.(PendingContractCaller) |
||||
if !ok { |
||||
return ErrNoPendingState |
||||
} |
||||
output, err = pb.PendingCallContract(ctx, msg) |
||||
if err == nil && len(output) == 0 { |
||||
// Make sure we have a contract to operate on, and bail out otherwise.
|
||||
if code, err = pb.PendingCodeAt(ctx, c.address); err != nil { |
||||
return err |
||||
} else if len(code) == 0 { |
||||
return ErrNoCode |
||||
} |
||||
} |
||||
} else { |
||||
output, err = c.caller.CallContract(ctx, msg, opts.BlockNumber) |
||||
if err == nil && len(output) == 0 { |
||||
// Make sure we have a contract to operate on, and bail out otherwise.
|
||||
if code, err = c.caller.CodeAt(ctx, c.address, opts.BlockNumber); err != nil { |
||||
return err |
||||
} else if len(code) == 0 { |
||||
return ErrNoCode |
||||
} |
||||
} |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return c.abi.Unpack(result, method, output) |
||||
} |
||||
|
||||
// Transact invokes the (paid) contract method with params as input values.
|
||||
func (c *BoundContract) Transact(opts *TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { |
||||
// Otherwise pack up the parameters and invoke the contract
|
||||
input, err := c.abi.Pack(method, params...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return c.transact(opts, &c.address, input) |
||||
} |
||||
|
||||
// Transfer initiates a plain transaction to move funds to the contract, calling
|
||||
// its default method if one is available.
|
||||
func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error) { |
||||
return c.transact(opts, &c.address, nil) |
||||
} |
||||
|
||||
// transact executes an actual transaction invocation, first deriving any missing
|
||||
// authorization fields, and then scheduling the transaction for execution.
|
||||
func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) { |
||||
var err error |
||||
|
||||
// Ensure a valid value field and resolve the account nonce
|
||||
value := opts.Value |
||||
if value == nil { |
||||
value = new(big.Int) |
||||
} |
||||
var nonce uint64 |
||||
if opts.Nonce == nil { |
||||
nonce, err = c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) |
||||
} |
||||
} else { |
||||
nonce = opts.Nonce.Uint64() |
||||
} |
||||
// Figure out the gas allowance and gas price values
|
||||
gasPrice := opts.GasPrice |
||||
if gasPrice == nil { |
||||
gasPrice, err = c.transactor.SuggestGasPrice(ensureContext(opts.Context)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to suggest gas price: %v", err) |
||||
} |
||||
} |
||||
gasLimit := opts.GasLimit |
||||
if gasLimit == 0 { |
||||
// Gas estimation cannot succeed without code for method invocations
|
||||
if contract != nil { |
||||
if code, err := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.address); err != nil { |
||||
return nil, err |
||||
} else if len(code) == 0 { |
||||
return nil, ErrNoCode |
||||
} |
||||
} |
||||
// If the contract surely has code (or code is not needed), estimate the transaction
|
||||
msg := ethereum.CallMsg{From: opts.From, To: contract, Value: value, Data: input} |
||||
gasLimit, err = c.transactor.EstimateGas(ensureContext(opts.Context), msg) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to estimate gas needed: %v", err) |
||||
} |
||||
} |
||||
// Create the transaction, sign it and schedule it for execution
|
||||
var rawTx *types.Transaction |
||||
if contract == nil { |
||||
rawTx = types.NewContractCreation(nonce, value, gasLimit, gasPrice, input) |
||||
} else { |
||||
rawTx = types.NewTransaction(nonce, c.address, value, gasLimit, gasPrice, input) |
||||
} |
||||
if opts.Signer == nil { |
||||
return nil, errors.New("no signer to authorize the transaction with") |
||||
} |
||||
signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil { |
||||
return nil, err |
||||
} |
||||
return signedTx, nil |
||||
} |
||||
|
||||
// FilterLogs filters contract logs for past blocks, returning the necessary
|
||||
// channels to construct a strongly typed bound iterator on top of them.
|
||||
func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]interface{}) (chan types.Log, event.Subscription, error) { |
||||
// Don't crash on a lazy user
|
||||
if opts == nil { |
||||
opts = new(FilterOpts) |
||||
} |
||||
// Append the event selector to the query parameters and construct the topic set
|
||||
query = append([][]interface{}{{c.abi.Events[name].Id()}}, query...) |
||||
|
||||
topics, err := makeTopics(query...) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
// Start the background filtering
|
||||
logs := make(chan types.Log, 128) |
||||
|
||||
config := ethereum.FilterQuery{ |
||||
Addresses: []common.Address{c.address}, |
||||
Topics: topics, |
||||
FromBlock: new(big.Int).SetUint64(opts.Start), |
||||
} |
||||
if opts.End != nil { |
||||
config.ToBlock = new(big.Int).SetUint64(*opts.End) |
||||
} |
||||
/* TODO(karalabe): Replace the rest of the method below with this when supported |
||||
sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) |
||||
*/ |
||||
buff, err := c.filterer.FilterLogs(ensureContext(opts.Context), config) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
sub, err := event.NewSubscription(func(quit <-chan struct{}) error { |
||||
for _, log := range buff { |
||||
select { |
||||
case logs <- log: |
||||
case <-quit: |
||||
return nil |
||||
} |
||||
} |
||||
return nil |
||||
}), nil |
||||
|
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return logs, sub, nil |
||||
} |
||||
|
||||
// WatchLogs filters subscribes to contract logs for future blocks, returning a
|
||||
// subscription object that can be used to tear down the watcher.
|
||||
func (c *BoundContract) WatchLogs(opts *WatchOpts, name string, query ...[]interface{}) (chan types.Log, event.Subscription, error) { |
||||
// Don't crash on a lazy user
|
||||
if opts == nil { |
||||
opts = new(WatchOpts) |
||||
} |
||||
// Append the event selector to the query parameters and construct the topic set
|
||||
query = append([][]interface{}{{c.abi.Events[name].Id()}}, query...) |
||||
|
||||
topics, err := makeTopics(query...) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
// Start the background filtering
|
||||
logs := make(chan types.Log, 128) |
||||
|
||||
config := ethereum.FilterQuery{ |
||||
Addresses: []common.Address{c.address}, |
||||
Topics: topics, |
||||
} |
||||
if opts.Start != nil { |
||||
config.FromBlock = new(big.Int).SetUint64(*opts.Start) |
||||
} |
||||
sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return logs, sub, nil |
||||
} |
||||
|
||||
// UnpackLog unpacks a retrieved log into the provided output structure.
|
||||
func (c *BoundContract) UnpackLog(out interface{}, event string, log types.Log) error { |
||||
if len(log.Data) > 0 { |
||||
if err := c.abi.Unpack(out, event, log.Data); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
var indexed abi.Arguments |
||||
for _, arg := range c.abi.Events[event].Inputs { |
||||
if arg.Indexed { |
||||
indexed = append(indexed, arg) |
||||
} |
||||
} |
||||
return parseTopics(out, indexed, log.Topics[1:]) |
||||
} |
||||
|
||||
// UnpackLogIntoMap unpacks a retrieved log into the provided map.
|
||||
func (c *BoundContract) UnpackLogIntoMap(out map[string]interface{}, event string, log types.Log) error { |
||||
if len(log.Data) > 0 { |
||||
if err := c.abi.UnpackIntoMap(out, event, log.Data); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
var indexed abi.Arguments |
||||
for _, arg := range c.abi.Events[event].Inputs { |
||||
if arg.Indexed { |
||||
indexed = append(indexed, arg) |
||||
} |
||||
} |
||||
return parseTopicsIntoMap(out, indexed, log.Topics[1:]) |
||||
} |
||||
|
||||
// ensureContext is a helper method to ensure a context is not nil, even if the
|
||||
// user specified it as such.
|
||||
func ensureContext(ctx context.Context) context.Context { |
||||
if ctx == nil { |
||||
return context.TODO() |
||||
} |
||||
return ctx |
||||
} |
@ -0,0 +1,373 @@ |
||||
// 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 bind_test |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"math/big" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||
"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/crypto" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
type mockCaller struct { |
||||
codeAtBlockNumber *big.Int |
||||
callContractBlockNumber *big.Int |
||||
} |
||||
|
||||
func (mc *mockCaller) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { |
||||
mc.codeAtBlockNumber = blockNumber |
||||
return []byte{1, 2, 3}, nil |
||||
} |
||||
|
||||
func (mc *mockCaller) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { |
||||
mc.callContractBlockNumber = blockNumber |
||||
return nil, nil |
||||
} |
||||
func TestPassingBlockNumber(t *testing.T) { |
||||
|
||||
mc := &mockCaller{} |
||||
|
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), abi.ABI{ |
||||
Methods: map[string]abi.Method{ |
||||
"something": { |
||||
Name: "something", |
||||
Outputs: abi.Arguments{}, |
||||
}, |
||||
}, |
||||
}, mc, nil, nil) |
||||
var ret string |
||||
|
||||
blockNumber := big.NewInt(42) |
||||
|
||||
bc.Call(&bind.CallOpts{BlockNumber: blockNumber}, &ret, "something") |
||||
|
||||
if mc.callContractBlockNumber != blockNumber { |
||||
t.Fatalf("CallContract() was not passed the block number") |
||||
} |
||||
|
||||
if mc.codeAtBlockNumber != blockNumber { |
||||
t.Fatalf("CodeAt() was not passed the block number") |
||||
} |
||||
|
||||
bc.Call(&bind.CallOpts{}, &ret, "something") |
||||
|
||||
if mc.callContractBlockNumber != nil { |
||||
t.Fatalf("CallContract() was passed a block number when it should not have been") |
||||
} |
||||
|
||||
if mc.codeAtBlockNumber != nil { |
||||
t.Fatalf("CodeAt() was passed a block number when it should not have been") |
||||
} |
||||
} |
||||
|
||||
const hexData = "0x000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158" |
||||
|
||||
func TestUnpackIndexedStringTyLogIntoMap(t *testing.T) { |
||||
hash := crypto.Keccak256Hash([]byte("testName")) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x0"), |
||||
hash, |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x0"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
|
||||
receivedMap := make(map[string]interface{}) |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"name": hash, |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if len(receivedMap) != 4 { |
||||
t.Fatal("unpacked map expected to have length 4") |
||||
} |
||||
if receivedMap["name"] != expectedReceivedMap["name"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIndexedSliceTyLogIntoMap(t *testing.T) { |
||||
sliceBytes, err := rlp.EncodeToBytes([]string{"name1", "name2", "name3", "name4"}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
hash := crypto.Keccak256Hash(sliceBytes) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x0"), |
||||
hash, |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x0"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"names","type":"string[]"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
|
||||
receivedMap := make(map[string]interface{}) |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"names": hash, |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if len(receivedMap) != 4 { |
||||
t.Fatal("unpacked map expected to have length 4") |
||||
} |
||||
if receivedMap["names"] != expectedReceivedMap["names"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIndexedArrayTyLogIntoMap(t *testing.T) { |
||||
arrBytes, err := rlp.EncodeToBytes([2]common.Address{common.HexToAddress("0x0"), common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2")}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
hash := crypto.Keccak256Hash(arrBytes) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x0"), |
||||
hash, |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x0"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"addresses","type":"address[2]"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
|
||||
receivedMap := make(map[string]interface{}) |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"addresses": hash, |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if len(receivedMap) != 4 { |
||||
t.Fatal("unpacked map expected to have length 4") |
||||
} |
||||
if receivedMap["addresses"] != expectedReceivedMap["addresses"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIndexedFuncTyLogIntoMap(t *testing.T) { |
||||
mockAddress := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2") |
||||
addrBytes := mockAddress.Bytes() |
||||
hash := crypto.Keccak256Hash([]byte("mockFunction(address,uint)")) |
||||
functionSelector := hash[:4] |
||||
functionTyBytes := append(addrBytes, functionSelector...) |
||||
var functionTy [24]byte |
||||
copy(functionTy[:], functionTyBytes[0:24]) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x99b5620489b6ef926d4518936cfec15d305452712b88bd59da2d9c10fb0953e8"), |
||||
common.BytesToHash(functionTyBytes), |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x5c698f13940a2153440c6d19660878bc90219d9298fdcf37365aa8d88d40fc42"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"function","type":"function"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
|
||||
receivedMap := make(map[string]interface{}) |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"function": functionTy, |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if len(receivedMap) != 4 { |
||||
t.Fatal("unpacked map expected to have length 4") |
||||
} |
||||
if receivedMap["function"] != expectedReceivedMap["function"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIndexedBytesTyLogIntoMap(t *testing.T) { |
||||
byts := []byte{1, 2, 3, 4, 5} |
||||
hash := crypto.Keccak256Hash(byts) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x99b5620489b6ef926d4518936cfec15d305452712b88bd59da2d9c10fb0953e8"), |
||||
hash, |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x5c698f13940a2153440c6d19660878bc90219d9298fdcf37365aa8d88d40fc42"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"content","type":"bytes"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
|
||||
receivedMap := make(map[string]interface{}) |
||||
expectedReceivedMap := map[string]interface{}{ |
||||
"content": hash, |
||||
"sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), |
||||
"amount": big.NewInt(1), |
||||
"memo": []byte{88}, |
||||
} |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if len(receivedMap) != 4 { |
||||
t.Fatal("unpacked map expected to have length 4") |
||||
} |
||||
if receivedMap["content"] != expectedReceivedMap["content"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["sender"] != expectedReceivedMap["sender"] { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if receivedMap["amount"].(*big.Int).Cmp(expectedReceivedMap["amount"].(*big.Int)) != 0 { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
if !bytes.Equal(receivedMap["memo"].([]byte), expectedReceivedMap["memo"].([]byte)) { |
||||
t.Error("unpacked map does not match expected map") |
||||
} |
||||
} |
||||
|
||||
func TestUnpackIntoMapNamingConflict(t *testing.T) { |
||||
hash := crypto.Keccak256Hash([]byte("testName")) |
||||
mockLog := types.Log{ |
||||
Address: common.HexToAddress("0x0"), |
||||
Topics: []common.Hash{ |
||||
common.HexToHash("0x0"), |
||||
hash, |
||||
}, |
||||
Data: hexutil.MustDecode(hexData), |
||||
BlockNumber: uint64(26), |
||||
TxHash: common.HexToHash("0x0"), |
||||
TxIndex: 111, |
||||
BlockHash: common.BytesToHash([]byte{1, 2, 3, 4, 5}), |
||||
Index: 7, |
||||
Removed: false, |
||||
} |
||||
|
||||
abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"received","type":"event"}]` |
||||
parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) |
||||
bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) |
||||
receivedMap := make(map[string]interface{}) |
||||
if err := bc.UnpackLogIntoMap(receivedMap, "received", mockLog); err == nil { |
||||
t.Error("naming conflict between two events; error expected") |
||||
} |
||||
} |
@ -0,0 +1,424 @@ |
||||
// Copyright 2016 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 bind generates Ethereum contract Go bindings.
|
||||
//
|
||||
// Detailed usage document and tutorial available on the go-ethereum Wiki page:
|
||||
// https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts
|
||||
package bind |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"go/format" |
||||
"regexp" |
||||
"strings" |
||||
"text/template" |
||||
"unicode" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
) |
||||
|
||||
// Lang is a target programming language selector to generate bindings for.
|
||||
type Lang int |
||||
|
||||
const ( |
||||
LangGo Lang = iota |
||||
LangJava |
||||
LangObjC |
||||
) |
||||
|
||||
// Bind generates a Go wrapper around a contract ABI. This wrapper isn't meant
|
||||
// to be used as is in client code, but rather as an intermediate struct which
|
||||
// enforces compile time type safety and naming convention opposed to having to
|
||||
// manually maintain hard coded strings that break on runtime.
|
||||
func Bind(types []string, abis []string, bytecodes []string, pkg string, lang Lang) (string, error) { |
||||
// Process each individual contract requested binding
|
||||
contracts := make(map[string]*tmplContract) |
||||
|
||||
for i := 0; i < len(types); i++ { |
||||
// Parse the actual ABI to generate the binding for
|
||||
evmABI, err := abi.JSON(strings.NewReader(abis[i])) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
// Strip any whitespace from the JSON ABI
|
||||
strippedABI := strings.Map(func(r rune) rune { |
||||
if unicode.IsSpace(r) { |
||||
return -1 |
||||
} |
||||
return r |
||||
}, abis[i]) |
||||
|
||||
// Extract the call and transact methods; events; and sort them alphabetically
|
||||
var ( |
||||
calls = make(map[string]*tmplMethod) |
||||
transacts = make(map[string]*tmplMethod) |
||||
events = make(map[string]*tmplEvent) |
||||
) |
||||
for _, original := range evmABI.Methods { |
||||
// Normalize the method for capital cases and non-anonymous inputs/outputs
|
||||
normalized := original |
||||
normalized.Name = methodNormalizer[lang](original.Name) |
||||
|
||||
normalized.Inputs = make([]abi.Argument, len(original.Inputs)) |
||||
copy(normalized.Inputs, original.Inputs) |
||||
for j, input := range normalized.Inputs { |
||||
if input.Name == "" { |
||||
normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) |
||||
} |
||||
} |
||||
normalized.Outputs = make([]abi.Argument, len(original.Outputs)) |
||||
copy(normalized.Outputs, original.Outputs) |
||||
for j, output := range normalized.Outputs { |
||||
if output.Name != "" { |
||||
normalized.Outputs[j].Name = capitalise(output.Name) |
||||
} |
||||
} |
||||
// Append the methods to the call or transact lists
|
||||
if original.Const { |
||||
calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} |
||||
} else { |
||||
transacts[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} |
||||
} |
||||
} |
||||
for _, original := range evmABI.Events { |
||||
// Skip anonymous events as they don't support explicit filtering
|
||||
if original.Anonymous { |
||||
continue |
||||
} |
||||
// Normalize the event for capital cases and non-anonymous outputs
|
||||
normalized := original |
||||
normalized.Name = methodNormalizer[lang](original.Name) |
||||
|
||||
normalized.Inputs = make([]abi.Argument, len(original.Inputs)) |
||||
copy(normalized.Inputs, original.Inputs) |
||||
for j, input := range normalized.Inputs { |
||||
// Indexed fields are input, non-indexed ones are outputs
|
||||
if input.Indexed { |
||||
if input.Name == "" { |
||||
normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) |
||||
} |
||||
} |
||||
} |
||||
// Append the event to the accumulator list
|
||||
events[original.Name] = &tmplEvent{Original: original, Normalized: normalized} |
||||
} |
||||
contracts[types[i]] = &tmplContract{ |
||||
Type: capitalise(types[i]), |
||||
InputABI: strings.Replace(strippedABI, "\"", "\\\"", -1), |
||||
InputBin: strings.TrimSpace(bytecodes[i]), |
||||
Constructor: evmABI.Constructor, |
||||
Calls: calls, |
||||
Transacts: transacts, |
||||
Events: events, |
||||
} |
||||
} |
||||
// Generate the contract template data content and render it
|
||||
data := &tmplData{ |
||||
Package: pkg, |
||||
Contracts: contracts, |
||||
} |
||||
buffer := new(bytes.Buffer) |
||||
|
||||
funcs := map[string]interface{}{ |
||||
"bindtype": bindType[lang], |
||||
"bindtopictype": bindTopicType[lang], |
||||
"namedtype": namedType[lang], |
||||
"capitalise": capitalise, |
||||
"decapitalise": decapitalise, |
||||
} |
||||
tmpl := template.Must(template.New("").Funcs(funcs).Parse(tmplSource[lang])) |
||||
if err := tmpl.Execute(buffer, data); err != nil { |
||||
return "", err |
||||
} |
||||
// For Go bindings pass the code through gofmt to clean it up
|
||||
if lang == LangGo { |
||||
code, err := format.Source(buffer.Bytes()) |
||||
if err != nil { |
||||
return "", fmt.Errorf("%v\n%s", err, buffer) |
||||
} |
||||
return string(code), nil |
||||
} |
||||
// For all others just return as is for now
|
||||
return buffer.String(), nil |
||||
} |
||||
|
||||
// bindType is a set of type binders that convert Solidity types to some supported
|
||||
// programming language types.
|
||||
var bindType = map[Lang]func(kind abi.Type) string{ |
||||
LangGo: bindTypeGo, |
||||
LangJava: bindTypeJava, |
||||
} |
||||
|
||||
// Helper function for the binding generators.
|
||||
// It reads the unmatched characters after the inner type-match,
|
||||
// (since the inner type is a prefix of the total type declaration),
|
||||
// looks for valid arrays (possibly a dynamic one) wrapping the inner type,
|
||||
// and returns the sizes of these arrays.
|
||||
//
|
||||
// Returned array sizes are in the same order as solidity signatures; inner array size first.
|
||||
// Array sizes may also be "", indicating a dynamic array.
|
||||
func wrapArray(stringKind string, innerLen int, innerMapping string) (string, []string) { |
||||
remainder := stringKind[innerLen:] |
||||
//find all the sizes
|
||||
matches := regexp.MustCompile(`\[(\d*)\]`).FindAllStringSubmatch(remainder, -1) |
||||
parts := make([]string, 0, len(matches)) |
||||
for _, match := range matches { |
||||
//get group 1 from the regex match
|
||||
parts = append(parts, match[1]) |
||||
} |
||||
return innerMapping, parts |
||||
} |
||||
|
||||
// Translates the array sizes to a Go-lang declaration of a (nested) array of the inner type.
|
||||
// Simply returns the inner type if arraySizes is empty.
|
||||
func arrayBindingGo(inner string, arraySizes []string) string { |
||||
out := "" |
||||
//prepend all array sizes, from outer (end arraySizes) to inner (start arraySizes)
|
||||
for i := len(arraySizes) - 1; i >= 0; i-- { |
||||
out += "[" + arraySizes[i] + "]" |
||||
} |
||||
out += inner |
||||
return out |
||||
} |
||||
|
||||
// bindTypeGo converts a Solidity type to a Go one. Since there is no clear mapping
|
||||
// from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly
|
||||
// mapped will use an upscaled type (e.g. *big.Int).
|
||||
func bindTypeGo(kind abi.Type) string { |
||||
stringKind := kind.String() |
||||
innerLen, innerMapping := bindUnnestedTypeGo(stringKind) |
||||
return arrayBindingGo(wrapArray(stringKind, innerLen, innerMapping)) |
||||
} |
||||
|
||||
// The inner function of bindTypeGo, this finds the inner type of stringKind.
|
||||
// (Or just the type itself if it is not an array or slice)
|
||||
// The length of the matched part is returned, with the translated type.
|
||||
func bindUnnestedTypeGo(stringKind string) (int, string) { |
||||
|
||||
switch { |
||||
case strings.HasPrefix(stringKind, "address"): |
||||
return len("address"), "common.Address" |
||||
|
||||
case strings.HasPrefix(stringKind, "bytes"): |
||||
parts := regexp.MustCompile(`bytes([0-9]*)`).FindStringSubmatch(stringKind) |
||||
return len(parts[0]), fmt.Sprintf("[%s]byte", parts[1]) |
||||
|
||||
case strings.HasPrefix(stringKind, "int") || strings.HasPrefix(stringKind, "uint"): |
||||
parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(stringKind) |
||||
switch parts[2] { |
||||
case "8", "16", "32", "64": |
||||
return len(parts[0]), fmt.Sprintf("%sint%s", parts[1], parts[2]) |
||||
} |
||||
return len(parts[0]), "*big.Int" |
||||
|
||||
case strings.HasPrefix(stringKind, "bool"): |
||||
return len("bool"), "bool" |
||||
|
||||
case strings.HasPrefix(stringKind, "string"): |
||||
return len("string"), "string" |
||||
|
||||
default: |
||||
return len(stringKind), stringKind |
||||
} |
||||
} |
||||
|
||||
// Translates the array sizes to a Java declaration of a (nested) array of the inner type.
|
||||
// Simply returns the inner type if arraySizes is empty.
|
||||
func arrayBindingJava(inner string, arraySizes []string) string { |
||||
// Java array type declarations do not include the length.
|
||||
return inner + strings.Repeat("[]", len(arraySizes)) |
||||
} |
||||
|
||||
// bindTypeJava converts a Solidity type to a Java one. Since there is no clear mapping
|
||||
// from all Solidity types to Java ones (e.g. uint17), those that cannot be exactly
|
||||
// mapped will use an upscaled type (e.g. BigDecimal).
|
||||
func bindTypeJava(kind abi.Type) string { |
||||
stringKind := kind.String() |
||||
innerLen, innerMapping := bindUnnestedTypeJava(stringKind) |
||||
return arrayBindingJava(wrapArray(stringKind, innerLen, innerMapping)) |
||||
} |
||||
|
||||
// The inner function of bindTypeJava, this finds the inner type of stringKind.
|
||||
// (Or just the type itself if it is not an array or slice)
|
||||
// The length of the matched part is returned, with the translated type.
|
||||
func bindUnnestedTypeJava(stringKind string) (int, string) { |
||||
|
||||
switch { |
||||
case strings.HasPrefix(stringKind, "address"): |
||||
parts := regexp.MustCompile(`address(\[[0-9]*\])?`).FindStringSubmatch(stringKind) |
||||
if len(parts) != 2 { |
||||
return len(stringKind), stringKind |
||||
} |
||||
if parts[1] == "" { |
||||
return len("address"), "Address" |
||||
} |
||||
return len(parts[0]), "Addresses" |
||||
|
||||
case strings.HasPrefix(stringKind, "bytes"): |
||||
parts := regexp.MustCompile(`bytes([0-9]*)`).FindStringSubmatch(stringKind) |
||||
if len(parts) != 2 { |
||||
return len(stringKind), stringKind |
||||
} |
||||
return len(parts[0]), "byte[]" |
||||
|
||||
case strings.HasPrefix(stringKind, "int") || strings.HasPrefix(stringKind, "uint"): |
||||
//Note that uint and int (without digits) are also matched,
|
||||
// these are size 256, and will translate to BigInt (the default).
|
||||
parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(stringKind) |
||||
if len(parts) != 3 { |
||||
return len(stringKind), stringKind |
||||
} |
||||
|
||||
namedSize := map[string]string{ |
||||
"8": "byte", |
||||
"16": "short", |
||||
"32": "int", |
||||
"64": "long", |
||||
}[parts[2]] |
||||
|
||||
//default to BigInt
|
||||
if namedSize == "" { |
||||
namedSize = "BigInt" |
||||
} |
||||
return len(parts[0]), namedSize |
||||
|
||||
case strings.HasPrefix(stringKind, "bool"): |
||||
return len("bool"), "boolean" |
||||
|
||||
case strings.HasPrefix(stringKind, "string"): |
||||
return len("string"), "String" |
||||
|
||||
default: |
||||
return len(stringKind), stringKind |
||||
} |
||||
} |
||||
|
||||
// bindTopicType is a set of type binders that convert Solidity types to some
|
||||
// supported programming language topic types.
|
||||
var bindTopicType = map[Lang]func(kind abi.Type) string{ |
||||
LangGo: bindTopicTypeGo, |
||||
LangJava: bindTopicTypeJava, |
||||
} |
||||
|
||||
// bindTypeGo converts a Solidity topic type to a Go one. It is almost the same
|
||||
// funcionality as for simple types, but dynamic types get converted to hashes.
|
||||
func bindTopicTypeGo(kind abi.Type) string { |
||||
bound := bindTypeGo(kind) |
||||
if bound == "string" || bound == "[]byte" { |
||||
bound = "common.Hash" |
||||
} |
||||
return bound |
||||
} |
||||
|
||||
// bindTypeGo converts a Solidity topic type to a Java one. It is almost the same
|
||||
// funcionality as for simple types, but dynamic types get converted to hashes.
|
||||
func bindTopicTypeJava(kind abi.Type) string { |
||||
bound := bindTypeJava(kind) |
||||
if bound == "String" || bound == "Bytes" { |
||||
bound = "Hash" |
||||
} |
||||
return bound |
||||
} |
||||
|
||||
// namedType is a set of functions that transform language specific types to
|
||||
// named versions that my be used inside method names.
|
||||
var namedType = map[Lang]func(string, abi.Type) string{ |
||||
LangGo: func(string, abi.Type) string { panic("this shouldn't be needed") }, |
||||
LangJava: namedTypeJava, |
||||
} |
||||
|
||||
// namedTypeJava converts some primitive data types to named variants that can
|
||||
// be used as parts of method names.
|
||||
func namedTypeJava(javaKind string, solKind abi.Type) string { |
||||
switch javaKind { |
||||
case "byte[]": |
||||
return "Binary" |
||||
case "byte[][]": |
||||
return "Binaries" |
||||
case "string": |
||||
return "String" |
||||
case "string[]": |
||||
return "Strings" |
||||
case "boolean": |
||||
return "Bool" |
||||
case "boolean[]": |
||||
return "Bools" |
||||
case "BigInt[]": |
||||
return "BigInts" |
||||
default: |
||||
parts := regexp.MustCompile(`(u)?int([0-9]*)(\[[0-9]*\])?`).FindStringSubmatch(solKind.String()) |
||||
if len(parts) != 4 { |
||||
return javaKind |
||||
} |
||||
switch parts[2] { |
||||
case "8", "16", "32", "64": |
||||
if parts[3] == "" { |
||||
return capitalise(fmt.Sprintf("%sint%s", parts[1], parts[2])) |
||||
} |
||||
return capitalise(fmt.Sprintf("%sint%ss", parts[1], parts[2])) |
||||
|
||||
default: |
||||
return javaKind |
||||
} |
||||
} |
||||
} |
||||
|
||||
// methodNormalizer is a name transformer that modifies Solidity method names to
|
||||
// conform to target language naming concentions.
|
||||
var methodNormalizer = map[Lang]func(string) string{ |
||||
LangGo: abi.ToCamelCase, |
||||
LangJava: decapitalise, |
||||
} |
||||
|
||||
// capitalise makes a camel-case string which starts with an upper case character.
|
||||
func capitalise(input string) string { |
||||
return abi.ToCamelCase(input) |
||||
} |
||||
|
||||
// decapitalise makes a camel-case string which starts with a lower case character.
|
||||
func decapitalise(input string) string { |
||||
if len(input) == 0 { |
||||
return input |
||||
} |
||||
|
||||
goForm := abi.ToCamelCase(input) |
||||
return strings.ToLower(goForm[:1]) + goForm[1:] |
||||
} |
||||
|
||||
// structured checks whether a list of ABI data types has enough information to
|
||||
// operate through a proper Go struct or if flat returns are needed.
|
||||
func structured(args abi.Arguments) bool { |
||||
if len(args) < 2 { |
||||
return false |
||||
} |
||||
exists := make(map[string]bool) |
||||
for _, out := range args { |
||||
// If the name is anonymous, we can't organize into a struct
|
||||
if out.Name == "" { |
||||
return false |
||||
} |
||||
// If the field name is empty when normalized or collides (var, Var, _var, _Var),
|
||||
// we can't organize into a struct
|
||||
field := capitalise(out.Name) |
||||
if field == "" || exists[field] { |
||||
return false |
||||
} |
||||
exists[field] = true |
||||
} |
||||
return true |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,546 @@ |
||||
// Copyright 2016 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 bind |
||||
|
||||
import "github.com/ethereum/go-ethereum/accounts/abi" |
||||
|
||||
// tmplData is the data structure required to fill the binding template.
|
||||
type tmplData struct { |
||||
Package string // Name of the package to place the generated file in
|
||||
Contracts map[string]*tmplContract // List of contracts to generate into this file
|
||||
} |
||||
|
||||
// tmplContract contains the data needed to generate an individual contract binding.
|
||||
type tmplContract struct { |
||||
Type string // Type name of the main contract binding
|
||||
InputABI string // JSON ABI used as the input to generate the binding from
|
||||
InputBin string // Optional EVM bytecode used to denetare deploy code from
|
||||
Constructor abi.Method // Contract constructor for deploy parametrization
|
||||
Calls map[string]*tmplMethod // Contract calls that only read state data
|
||||
Transacts map[string]*tmplMethod // Contract calls that write state data
|
||||
Events map[string]*tmplEvent // Contract events accessors
|
||||
} |
||||
|
||||
// tmplMethod is a wrapper around an abi.Method that contains a few preprocessed
|
||||
// and cached data fields.
|
||||
type tmplMethod struct { |
||||
Original abi.Method // Original method as parsed by the abi package
|
||||
Normalized abi.Method // Normalized version of the parsed method (capitalized names, non-anonymous args/returns)
|
||||
Structured bool // Whether the returns should be accumulated into a struct
|
||||
} |
||||
|
||||
// tmplEvent is a wrapper around an a
|
||||
type tmplEvent struct { |
||||
Original abi.Event // Original event as parsed by the abi package
|
||||
Normalized abi.Event // Normalized version of the parsed fields
|
||||
} |
||||
|
||||
// tmplSource is language to template mapping containing all the supported
|
||||
// programming languages the package can generate to.
|
||||
var tmplSource = map[Lang]string{ |
||||
LangGo: tmplSourceGo, |
||||
LangJava: tmplSourceJava, |
||||
} |
||||
|
||||
// tmplSourceGo is the Go source template use to generate the contract binding
|
||||
// based on.
|
||||
const tmplSourceGo = ` |
||||
// Code generated - DO NOT EDIT.
|
||||
// This file is a generated binding and any manual changes will be lost.
|
||||
|
||||
package {{.Package}} |
||||
|
||||
import ( |
||||
"math/big" |
||||
"strings" |
||||
|
||||
ethereum "github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
) |
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var ( |
||||
_ = big.NewInt |
||||
_ = strings.NewReader |
||||
_ = ethereum.NotFound |
||||
_ = abi.U256 |
||||
_ = bind.Bind |
||||
_ = common.Big1 |
||||
_ = types.BloomLookup |
||||
_ = event.NewSubscription |
||||
) |
||||
|
||||
{{range $contract := .Contracts}} |
||||
// {{.Type}}ABI is the input ABI used to generate the binding from.
|
||||
const {{.Type}}ABI = "{{.InputABI}}" |
||||
|
||||
{{if .InputBin}} |
||||
// {{.Type}}Bin is the compiled bytecode used for deploying new contracts.
|
||||
const {{.Type}}Bin = ` + "`" + `{{.InputBin}}` + "`" + ` |
||||
|
||||
// Deploy{{.Type}} deploys a new Ethereum contract, binding an instance of {{.Type}} to it.
|
||||
func Deploy{{.Type}}(auth *bind.TransactOpts, backend bind.ContractBackend {{range .Constructor.Inputs}}, {{.Name}} {{bindtype .Type}}{{end}}) (common.Address, *types.Transaction, *{{.Type}}, error) { |
||||
parsed, err := abi.JSON(strings.NewReader({{.Type}}ABI)) |
||||
if err != nil { |
||||
return common.Address{}, nil, nil, err |
||||
} |
||||
address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex({{.Type}}Bin), backend {{range .Constructor.Inputs}}, {{.Name}}{{end}}) |
||||
if err != nil { |
||||
return common.Address{}, nil, nil, err |
||||
} |
||||
return address, tx, &{{.Type}}{ {{.Type}}Caller: {{.Type}}Caller{contract: contract}, {{.Type}}Transactor: {{.Type}}Transactor{contract: contract}, {{.Type}}Filterer: {{.Type}}Filterer{contract: contract} }, nil |
||||
} |
||||
{{end}} |
||||
|
||||
// {{.Type}} is an auto generated Go binding around an Ethereum contract.
|
||||
type {{.Type}} struct { |
||||
{{.Type}}Caller // Read-only binding to the contract
|
||||
{{.Type}}Transactor // Write-only binding to the contract
|
||||
{{.Type}}Filterer // Log filterer for contract events
|
||||
} |
||||
|
||||
// {{.Type}}Caller is an auto generated read-only Go binding around an Ethereum contract.
|
||||
type {{.Type}}Caller struct { |
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
} |
||||
|
||||
// {{.Type}}Transactor is an auto generated write-only Go binding around an Ethereum contract.
|
||||
type {{.Type}}Transactor struct { |
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
} |
||||
|
||||
// {{.Type}}Filterer is an auto generated log filtering Go binding around an Ethereum contract events.
|
||||
type {{.Type}}Filterer struct { |
||||
contract *bind.BoundContract // Generic contract wrapper for the low level calls
|
||||
} |
||||
|
||||
// {{.Type}}Session is an auto generated Go binding around an Ethereum contract,
|
||||
// with pre-set call and transact options.
|
||||
type {{.Type}}Session struct { |
||||
Contract *{{.Type}} // Generic contract binding to set the session for
|
||||
CallOpts bind.CallOpts // Call options to use throughout this session
|
||||
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
|
||||
} |
||||
|
||||
// {{.Type}}CallerSession is an auto generated read-only Go binding around an Ethereum contract,
|
||||
// with pre-set call options.
|
||||
type {{.Type}}CallerSession struct { |
||||
Contract *{{.Type}}Caller // Generic contract caller binding to set the session for
|
||||
CallOpts bind.CallOpts // Call options to use throughout this session
|
||||
} |
||||
|
||||
// {{.Type}}TransactorSession is an auto generated write-only Go binding around an Ethereum contract,
|
||||
// with pre-set transact options.
|
||||
type {{.Type}}TransactorSession struct { |
||||
Contract *{{.Type}}Transactor // Generic contract transactor binding to set the session for
|
||||
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
|
||||
} |
||||
|
||||
// {{.Type}}Raw is an auto generated low-level Go binding around an Ethereum contract.
|
||||
type {{.Type}}Raw struct { |
||||
Contract *{{.Type}} // Generic contract binding to access the raw methods on
|
||||
} |
||||
|
||||
// {{.Type}}CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
|
||||
type {{.Type}}CallerRaw struct { |
||||
Contract *{{.Type}}Caller // Generic read-only contract binding to access the raw methods on
|
||||
} |
||||
|
||||
// {{.Type}}TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
|
||||
type {{.Type}}TransactorRaw struct { |
||||
Contract *{{.Type}}Transactor // Generic write-only contract binding to access the raw methods on
|
||||
} |
||||
|
||||
// New{{.Type}} creates a new instance of {{.Type}}, bound to a specific deployed contract.
|
||||
func New{{.Type}}(address common.Address, backend bind.ContractBackend) (*{{.Type}}, error) { |
||||
contract, err := bind{{.Type}}(address, backend, backend, backend) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &{{.Type}}{ {{.Type}}Caller: {{.Type}}Caller{contract: contract}, {{.Type}}Transactor: {{.Type}}Transactor{contract: contract}, {{.Type}}Filterer: {{.Type}}Filterer{contract: contract} }, nil |
||||
} |
||||
|
||||
// New{{.Type}}Caller creates a new read-only instance of {{.Type}}, bound to a specific deployed contract.
|
||||
func New{{.Type}}Caller(address common.Address, caller bind.ContractCaller) (*{{.Type}}Caller, error) { |
||||
contract, err := bind{{.Type}}(address, caller, nil, nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &{{.Type}}Caller{contract: contract}, nil |
||||
} |
||||
|
||||
// New{{.Type}}Transactor creates a new write-only instance of {{.Type}}, bound to a specific deployed contract.
|
||||
func New{{.Type}}Transactor(address common.Address, transactor bind.ContractTransactor) (*{{.Type}}Transactor, error) { |
||||
contract, err := bind{{.Type}}(address, nil, transactor, nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &{{.Type}}Transactor{contract: contract}, nil |
||||
} |
||||
|
||||
// New{{.Type}}Filterer creates a new log filterer instance of {{.Type}}, bound to a specific deployed contract.
|
||||
func New{{.Type}}Filterer(address common.Address, filterer bind.ContractFilterer) (*{{.Type}}Filterer, error) { |
||||
contract, err := bind{{.Type}}(address, nil, nil, filterer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &{{.Type}}Filterer{contract: contract}, nil |
||||
} |
||||
|
||||
// bind{{.Type}} binds a generic wrapper to an already deployed contract.
|
||||
func bind{{.Type}}(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { |
||||
parsed, err := abi.JSON(strings.NewReader({{.Type}}ABI)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil |
||||
} |
||||
|
||||
// Call invokes the (constant) contract method with params as input values and
|
||||
// sets the output to result. The result type might be a single field for simple
|
||||
// returns, a slice of interfaces for anonymous returns and a struct for named
|
||||
// returns.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Raw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { |
||||
return _{{$contract.Type}}.Contract.{{$contract.Type}}Caller.contract.Call(opts, result, method, params...) |
||||
} |
||||
|
||||
// Transfer initiates a plain transaction to move funds to the contract, calling
|
||||
// its default method if one is available.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.{{$contract.Type}}Transactor.contract.Transfer(opts) |
||||
} |
||||
|
||||
// Transact invokes the (paid) contract method with params as input values.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.{{$contract.Type}}Transactor.contract.Transact(opts, method, params...) |
||||
} |
||||
|
||||
// Call invokes the (constant) contract method with params as input values and
|
||||
// sets the output to result. The result type might be a single field for simple
|
||||
// returns, a slice of interfaces for anonymous returns and a struct for named
|
||||
// returns.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}CallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { |
||||
return _{{$contract.Type}}.Contract.contract.Call(opts, result, method, params...) |
||||
} |
||||
|
||||
// Transfer initiates a plain transaction to move funds to the contract, calling
|
||||
// its default method if one is available.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.contract.Transfer(opts) |
||||
} |
||||
|
||||
// Transact invokes the (paid) contract method with params as input values.
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.contract.Transact(opts, method, params...) |
||||
} |
||||
|
||||
{{range .Calls}} |
||||
// {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Caller) {{.Normalized.Name}}(opts *bind.CallOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type}},{{end}}{{end}} error) { |
||||
{{if .Structured}}ret := new(struct{ |
||||
{{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}} |
||||
{{end}} |
||||
}){{else}}var ( |
||||
{{range $i, $_ := .Normalized.Outputs}}ret{{$i}} = new({{bindtype .Type}}) |
||||
{{end}} |
||||
){{end}} |
||||
out := {{if .Structured}}ret{{else}}{{if eq (len .Normalized.Outputs) 1}}ret0{{else}}&[]interface{}{ |
||||
{{range $i, $_ := .Normalized.Outputs}}ret{{$i}}, |
||||
{{end}} |
||||
}{{end}}{{end}} |
||||
err := _{{$contract.Type}}.contract.Call(opts, out, "{{.Original.Name}}" {{range .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
return {{if .Structured}}*ret,{{else}}{{range $i, $_ := .Normalized.Outputs}}*ret{{$i}},{{end}}{{end}} err |
||||
} |
||||
|
||||
// {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type}},{{end}} {{end}} error) { |
||||
return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.CallOpts {{range .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
} |
||||
|
||||
// {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}CallerSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type}},{{end}} {{end}} error) { |
||||
return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.CallOpts {{range .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
} |
||||
{{end}} |
||||
|
||||
{{range .Transacts}} |
||||
// {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Transactor) {{.Normalized.Name}}(opts *bind.TransactOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.contract.Transact(opts, "{{.Original.Name}}" {{range .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
} |
||||
|
||||
// {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
} |
||||
|
||||
// {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}TransactorSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { |
||||
return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) |
||||
} |
||||
{{end}} |
||||
|
||||
{{range .Events}} |
||||
// {{$contract.Type}}{{.Normalized.Name}}Iterator is returned from Filter{{.Normalized.Name}} and is used to iterate over the raw logs and unpacked data for {{.Normalized.Name}} events raised by the {{$contract.Type}} contract.
|
||||
type {{$contract.Type}}{{.Normalized.Name}}Iterator struct { |
||||
Event *{{$contract.Type}}{{.Normalized.Name}} // Event containing the contract specifics and raw log
|
||||
|
||||
contract *bind.BoundContract // Generic contract to use for unpacking event data
|
||||
event string // Event name to use for unpacking event data
|
||||
|
||||
logs chan types.Log // Log channel receiving the found contract events
|
||||
sub ethereum.Subscription // Subscription for errors, completion and termination
|
||||
done bool // Whether the subscription completed delivering logs
|
||||
fail error // Occurred error to stop iteration
|
||||
} |
||||
// Next advances the iterator to the subsequent event, returning whether there
|
||||
// are any more events found. In case of a retrieval or parsing error, false is
|
||||
// returned and Error() can be queried for the exact failure.
|
||||
func (it *{{$contract.Type}}{{.Normalized.Name}}Iterator) Next() bool { |
||||
// If the iterator failed, stop iterating
|
||||
if (it.fail != nil) { |
||||
return false |
||||
} |
||||
// If the iterator completed, deliver directly whatever's available
|
||||
if (it.done) { |
||||
select { |
||||
case log := <-it.logs: |
||||
it.Event = new({{$contract.Type}}{{.Normalized.Name}}) |
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { |
||||
it.fail = err |
||||
return false |
||||
} |
||||
it.Event.Raw = log |
||||
return true |
||||
|
||||
default: |
||||
return false |
||||
} |
||||
} |
||||
// Iterator still in progress, wait for either a data or an error event
|
||||
select { |
||||
case log := <-it.logs: |
||||
it.Event = new({{$contract.Type}}{{.Normalized.Name}}) |
||||
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { |
||||
it.fail = err |
||||
return false |
||||
} |
||||
it.Event.Raw = log |
||||
return true |
||||
|
||||
case err := <-it.sub.Err(): |
||||
it.done = true |
||||
it.fail = err |
||||
return it.Next() |
||||
} |
||||
} |
||||
// Error returns any retrieval or parsing error occurred during filtering.
|
||||
func (it *{{$contract.Type}}{{.Normalized.Name}}Iterator) Error() error { |
||||
return it.fail |
||||
} |
||||
// Close terminates the iteration process, releasing any pending underlying
|
||||
// resources.
|
||||
func (it *{{$contract.Type}}{{.Normalized.Name}}Iterator) Close() error { |
||||
it.sub.Unsubscribe() |
||||
return nil |
||||
} |
||||
|
||||
// {{$contract.Type}}{{.Normalized.Name}} represents a {{.Normalized.Name}} event raised by the {{$contract.Type}} contract.
|
||||
type {{$contract.Type}}{{.Normalized.Name}} struct { {{range .Normalized.Inputs}} |
||||
{{capitalise .Name}} {{if .Indexed}}{{bindtopictype .Type}}{{else}}{{bindtype .Type}}{{end}}; {{end}} |
||||
Raw types.Log // Blockchain specific contextual infos
|
||||
} |
||||
|
||||
// Filter{{.Normalized.Name}} is a free log retrieval operation binding the contract event 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Filter{{.Normalized.Name}}(opts *bind.FilterOpts{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type}}{{end}}{{end}}) (*{{$contract.Type}}{{.Normalized.Name}}Iterator, error) { |
||||
{{range .Normalized.Inputs}} |
||||
{{if .Indexed}}var {{.Name}}Rule []interface{} |
||||
for _, {{.Name}}Item := range {{.Name}} { |
||||
{{.Name}}Rule = append({{.Name}}Rule, {{.Name}}Item) |
||||
}{{end}}{{end}} |
||||
|
||||
logs, sub, err := _{{$contract.Type}}.contract.FilterLogs(opts, "{{.Original.Name}}"{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}}Rule{{end}}{{end}}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &{{$contract.Type}}{{.Normalized.Name}}Iterator{contract: _{{$contract.Type}}.contract, event: "{{.Original.Name}}", logs: logs, sub: sub}, nil |
||||
} |
||||
|
||||
// Watch{{.Normalized.Name}} is a free log subscription operation binding the contract event 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Watch{{.Normalized.Name}}(opts *bind.WatchOpts, sink chan<- *{{$contract.Type}}{{.Normalized.Name}}{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type}}{{end}}{{end}}) (event.Subscription, error) { |
||||
{{range .Normalized.Inputs}} |
||||
{{if .Indexed}}var {{.Name}}Rule []interface{} |
||||
for _, {{.Name}}Item := range {{.Name}} { |
||||
{{.Name}}Rule = append({{.Name}}Rule, {{.Name}}Item) |
||||
}{{end}}{{end}} |
||||
|
||||
logs, sub, err := _{{$contract.Type}}.contract.WatchLogs(opts, "{{.Original.Name}}"{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}}Rule{{end}}{{end}}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return event.NewSubscription(func(quit <-chan struct{}) error { |
||||
defer sub.Unsubscribe() |
||||
for { |
||||
select { |
||||
case log := <-logs: |
||||
// New log arrived, parse the event and forward to the user
|
||||
event := new({{$contract.Type}}{{.Normalized.Name}}) |
||||
if err := _{{$contract.Type}}.contract.UnpackLog(event, "{{.Original.Name}}", log); err != nil { |
||||
return err |
||||
} |
||||
event.Raw = log |
||||
|
||||
select { |
||||
case sink <- event: |
||||
case err := <-sub.Err(): |
||||
return err |
||||
case <-quit: |
||||
return nil |
||||
} |
||||
case err := <-sub.Err(): |
||||
return err |
||||
case <-quit: |
||||
return nil |
||||
} |
||||
} |
||||
}), nil |
||||
} |
||||
{{end}} |
||||
{{end}} |
||||
` |
||||
|
||||
// tmplSourceJava is the Java source template use to generate the contract binding
|
||||
// based on.
|
||||
const tmplSourceJava = ` |
||||
// This file is an automatically generated Java binding. Do not modify as any
|
||||
// change will likely be lost upon the next re-generation!
|
||||
|
||||
package {{.Package}}; |
||||
|
||||
import org.ethereum.geth.*; |
||||
import org.ethereum.geth.internal.*; |
||||
|
||||
{{range $contract := .Contracts}} |
||||
public class {{.Type}} { |
||||
// ABI is the input ABI used to generate the binding from.
|
||||
public final static String ABI = "{{.InputABI}}"; |
||||
|
||||
{{if .InputBin}} |
||||
// BYTECODE is the compiled bytecode used for deploying new contracts.
|
||||
public final static byte[] BYTECODE = "{{.InputBin}}".getBytes(); |
||||
|
||||
// deploy deploys a new Ethereum contract, binding an instance of {{.Type}} to it.
|
||||
public static {{.Type}} deploy(TransactOpts auth, EthereumClient client{{range .Constructor.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { |
||||
Interfaces args = Geth.newInterfaces({{(len .Constructor.Inputs)}}); |
||||
{{range $index, $element := .Constructor.Inputs}} |
||||
args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); |
||||
{{end}} |
||||
return new {{.Type}}(Geth.deployContract(auth, ABI, BYTECODE, client, args)); |
||||
} |
||||
|
||||
// Internal constructor used by contract deployment.
|
||||
private {{.Type}}(BoundContract deployment) { |
||||
this.Address = deployment.getAddress(); |
||||
this.Deployer = deployment.getDeployer(); |
||||
this.Contract = deployment; |
||||
} |
||||
{{end}} |
||||
|
||||
// Ethereum address where this contract is located at.
|
||||
public final Address Address; |
||||
|
||||
// Ethereum transaction in which this contract was deployed (if known!).
|
||||
public final Transaction Deployer; |
||||
|
||||
// Contract instance bound to a blockchain address.
|
||||
private final BoundContract Contract; |
||||
|
||||
// Creates a new instance of {{.Type}}, bound to a specific deployed contract.
|
||||
public {{.Type}}(Address address, EthereumClient client) throws Exception { |
||||
this(Geth.bindContract(address, ABI, client)); |
||||
} |
||||
|
||||
{{range .Calls}} |
||||
{{if gt (len .Normalized.Outputs) 1}} |
||||
// {{capitalise .Normalized.Name}}Results is the output of a call to {{.Normalized.Name}}.
|
||||
public class {{capitalise .Normalized.Name}}Results { |
||||
{{range $index, $item := .Normalized.Outputs}}public {{bindtype .Type}} {{if ne .Name ""}}{{.Name}}{{else}}Return{{$index}}{{end}}; |
||||
{{end}} |
||||
} |
||||
{{end}} |
||||
|
||||
// {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
public {{if gt (len .Normalized.Outputs) 1}}{{capitalise .Normalized.Name}}Results{{else}}{{range .Normalized.Outputs}}{{bindtype .Type}}{{end}}{{end}} {{.Normalized.Name}}(CallOpts opts{{range .Normalized.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { |
||||
Interfaces args = Geth.newInterfaces({{(len .Normalized.Inputs)}}); |
||||
{{range $index, $item := .Normalized.Inputs}}args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); |
||||
{{end}} |
||||
|
||||
Interfaces results = Geth.newInterfaces({{(len .Normalized.Outputs)}}); |
||||
{{range $index, $item := .Normalized.Outputs}}Interface result{{$index}} = Geth.newInterface(); result{{$index}}.setDefault{{namedtype (bindtype .Type) .Type}}(); results.set({{$index}}, result{{$index}}); |
||||
{{end}} |
||||
|
||||
if (opts == null) { |
||||
opts = Geth.newCallOpts(); |
||||
} |
||||
this.Contract.call(opts, results, "{{.Original.Name}}", args); |
||||
{{if gt (len .Normalized.Outputs) 1}} |
||||
{{capitalise .Normalized.Name}}Results result = new {{capitalise .Normalized.Name}}Results(); |
||||
{{range $index, $item := .Normalized.Outputs}}result.{{if ne .Name ""}}{{.Name}}{{else}}Return{{$index}}{{end}} = results.get({{$index}}).get{{namedtype (bindtype .Type) .Type}}(); |
||||
{{end}} |
||||
return result; |
||||
{{else}}{{range .Normalized.Outputs}}return results.get(0).get{{namedtype (bindtype .Type) .Type}}();{{end}} |
||||
{{end}} |
||||
} |
||||
{{end}} |
||||
|
||||
{{range .Transacts}} |
||||
// {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}.
|
||||
//
|
||||
// Solidity: {{.Original.String}}
|
||||
public Transaction {{.Normalized.Name}}(TransactOpts opts{{range .Normalized.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { |
||||
Interfaces args = Geth.newInterfaces({{(len .Normalized.Inputs)}}); |
||||
{{range $index, $item := .Normalized.Inputs}}args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); |
||||
{{end}} |
||||
|
||||
return this.Contract.transact(opts, "{{.Original.Name}}" , args); |
||||
} |
||||
{{end}} |
||||
} |
||||
{{end}} |
||||
` |
@ -0,0 +1,241 @@ |
||||
// 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 bind |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// makeTopics converts a filter query argument list into a filter topic set.
|
||||
func makeTopics(query ...[]interface{}) ([][]common.Hash, error) { |
||||
topics := make([][]common.Hash, len(query)) |
||||
for i, filter := range query { |
||||
for _, rule := range filter { |
||||
var topic common.Hash |
||||
|
||||
// Try to generate the topic based on simple types
|
||||
switch rule := rule.(type) { |
||||
case common.Hash: |
||||
copy(topic[:], rule[:]) |
||||
case common.Address: |
||||
copy(topic[common.HashLength-common.AddressLength:], rule[:]) |
||||
case *big.Int: |
||||
blob := rule.Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case bool: |
||||
if rule { |
||||
topic[common.HashLength-1] = 1 |
||||
} |
||||
case int8: |
||||
blob := big.NewInt(int64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case int16: |
||||
blob := big.NewInt(int64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case int32: |
||||
blob := big.NewInt(int64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case int64: |
||||
blob := big.NewInt(rule).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case uint8: |
||||
blob := new(big.Int).SetUint64(uint64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case uint16: |
||||
blob := new(big.Int).SetUint64(uint64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case uint32: |
||||
blob := new(big.Int).SetUint64(uint64(rule)).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case uint64: |
||||
blob := new(big.Int).SetUint64(rule).Bytes() |
||||
copy(topic[common.HashLength-len(blob):], blob) |
||||
case string: |
||||
hash := crypto.Keccak256Hash([]byte(rule)) |
||||
copy(topic[:], hash[:]) |
||||
case []byte: |
||||
hash := crypto.Keccak256Hash(rule) |
||||
copy(topic[:], hash[:]) |
||||
|
||||
default: |
||||
// Attempt to generate the topic from funky types
|
||||
val := reflect.ValueOf(rule) |
||||
|
||||
switch { |
||||
|
||||
// static byte array
|
||||
case val.Kind() == reflect.Array && reflect.TypeOf(rule).Elem().Kind() == reflect.Uint8: |
||||
reflect.Copy(reflect.ValueOf(topic[:val.Len()]), val) |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unsupported indexed type: %T", rule) |
||||
} |
||||
} |
||||
topics[i] = append(topics[i], topic) |
||||
} |
||||
} |
||||
return topics, nil |
||||
} |
||||
|
||||
// Big batch of reflect types for topic reconstruction.
|
||||
var ( |
||||
reflectHash = reflect.TypeOf(common.Hash{}) |
||||
reflectAddress = reflect.TypeOf(common.Address{}) |
||||
reflectBigInt = reflect.TypeOf(new(big.Int)) |
||||
) |
||||
|
||||
// parseTopics converts the indexed topic fields into actual log field values.
|
||||
//
|
||||
// Note, dynamic types cannot be reconstructed since they get mapped to Keccak256
|
||||
// hashes as the topic value!
|
||||
func parseTopics(out interface{}, fields abi.Arguments, topics []common.Hash) error { |
||||
// Sanity check that the fields and topics match up
|
||||
if len(fields) != len(topics) { |
||||
return errors.New("topic/field count mismatch") |
||||
} |
||||
// Iterate over all the fields and reconstruct them from topics
|
||||
for _, arg := range fields { |
||||
if !arg.Indexed { |
||||
return errors.New("non-indexed field in topic reconstruction") |
||||
} |
||||
field := reflect.ValueOf(out).Elem().FieldByName(capitalise(arg.Name)) |
||||
|
||||
// Try to parse the topic back into the fields based on primitive types
|
||||
switch field.Kind() { |
||||
case reflect.Bool: |
||||
if topics[0][common.HashLength-1] == 1 { |
||||
field.Set(reflect.ValueOf(true)) |
||||
} |
||||
case reflect.Int8: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(int8(num.Int64()))) |
||||
|
||||
case reflect.Int16: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(int16(num.Int64()))) |
||||
|
||||
case reflect.Int32: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(int32(num.Int64()))) |
||||
|
||||
case reflect.Int64: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(num.Int64())) |
||||
|
||||
case reflect.Uint8: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(uint8(num.Uint64()))) |
||||
|
||||
case reflect.Uint16: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(uint16(num.Uint64()))) |
||||
|
||||
case reflect.Uint32: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(uint32(num.Uint64()))) |
||||
|
||||
case reflect.Uint64: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(num.Uint64())) |
||||
|
||||
default: |
||||
// Ran out of plain primitive types, try custom types
|
||||
switch field.Type() { |
||||
case reflectHash: // Also covers all dynamic types
|
||||
field.Set(reflect.ValueOf(topics[0])) |
||||
|
||||
case reflectAddress: |
||||
var addr common.Address |
||||
copy(addr[:], topics[0][common.HashLength-common.AddressLength:]) |
||||
field.Set(reflect.ValueOf(addr)) |
||||
|
||||
case reflectBigInt: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
field.Set(reflect.ValueOf(num)) |
||||
|
||||
default: |
||||
// Ran out of custom types, try the crazies
|
||||
switch { |
||||
|
||||
// static byte array
|
||||
case arg.Type.T == abi.FixedBytesTy: |
||||
reflect.Copy(field, reflect.ValueOf(topics[0][:arg.Type.Size])) |
||||
|
||||
default: |
||||
return fmt.Errorf("unsupported indexed type: %v", arg.Type) |
||||
} |
||||
} |
||||
} |
||||
topics = topics[1:] |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// parseTopicsIntoMap converts the indexed topic field-value pairs into map key-value pairs
|
||||
func parseTopicsIntoMap(out map[string]interface{}, fields abi.Arguments, topics []common.Hash) error { |
||||
// Sanity check that the fields and topics match up
|
||||
if len(fields) != len(topics) { |
||||
return errors.New("topic/field count mismatch") |
||||
} |
||||
// Iterate over all the fields and reconstruct them from topics
|
||||
for _, arg := range fields { |
||||
if !arg.Indexed { |
||||
return errors.New("non-indexed field in topic reconstruction") |
||||
} |
||||
|
||||
switch arg.Type.T { |
||||
case abi.BoolTy: |
||||
out[arg.Name] = topics[0][common.HashLength-1] == 1 |
||||
case abi.IntTy, abi.UintTy: |
||||
num := new(big.Int).SetBytes(topics[0][:]) |
||||
out[arg.Name] = num |
||||
case abi.AddressTy: |
||||
var addr common.Address |
||||
copy(addr[:], topics[0][common.HashLength-common.AddressLength:]) |
||||
out[arg.Name] = addr |
||||
case abi.HashTy: |
||||
out[arg.Name] = topics[0] |
||||
case abi.FixedBytesTy: |
||||
out[arg.Name] = topics[0][:] |
||||
case abi.StringTy, abi.BytesTy, abi.SliceTy, abi.ArrayTy: |
||||
// Array types (including strings and bytes) have their keccak256 hashes stored in the topic- not a hash
|
||||
// whose bytes can be decoded to the actual value- so the best we can do is retrieve that hash
|
||||
out[arg.Name] = topics[0] |
||||
case abi.FunctionTy: |
||||
if garbage := binary.BigEndian.Uint64(topics[0][0:8]); garbage != 0 { |
||||
return fmt.Errorf("bind: got improperly encoded function type, got %v", topics[0].Bytes()) |
||||
} |
||||
var tmp [24]byte |
||||
copy(tmp[:], topics[0][8:32]) |
||||
out[arg.Name] = tmp |
||||
default: // Not handling tuples
|
||||
return fmt.Errorf("unsupported indexed type: %v", arg.Type) |
||||
} |
||||
|
||||
topics = topics[1:] |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,103 @@ |
||||
// 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 bind |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
func TestMakeTopics(t *testing.T) { |
||||
type args struct { |
||||
query [][]interface{} |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
want [][]common.Hash |
||||
wantErr bool |
||||
}{ |
||||
{ |
||||
"support fixed byte types, right padded to 32 bytes", |
||||
args{[][]interface{}{{[5]byte{1, 2, 3, 4, 5}}}}, |
||||
[][]common.Hash{{common.Hash{1, 2, 3, 4, 5}}}, |
||||
false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
got, err := makeTopics(tt.args.query...) |
||||
if (err != nil) != tt.wantErr { |
||||
t.Errorf("makeTopics() error = %v, wantErr %v", err, tt.wantErr) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("makeTopics() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestParseTopics(t *testing.T) { |
||||
type bytesStruct struct { |
||||
StaticBytes [5]byte |
||||
} |
||||
bytesType, _ := abi.NewType("bytes5", nil) |
||||
type args struct { |
||||
createObj func() interface{} |
||||
resultObj func() interface{} |
||||
fields abi.Arguments |
||||
topics []common.Hash |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
{ |
||||
name: "support fixed byte types, right padded to 32 bytes", |
||||
args: args{ |
||||
createObj: func() interface{} { return &bytesStruct{} }, |
||||
resultObj: func() interface{} { return &bytesStruct{StaticBytes: [5]byte{1, 2, 3, 4, 5}} }, |
||||
fields: abi.Arguments{abi.Argument{ |
||||
Name: "staticBytes", |
||||
Type: bytesType, |
||||
Indexed: true, |
||||
}}, |
||||
topics: []common.Hash{ |
||||
{1, 2, 3, 4, 5}, |
||||
}, |
||||
}, |
||||
wantErr: false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
createObj := tt.args.createObj() |
||||
if err := parseTopics(createObj, tt.args.fields, tt.args.topics); (err != nil) != tt.wantErr { |
||||
t.Errorf("parseTopics() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
resultObj := tt.args.resultObj() |
||||
if !reflect.DeepEqual(createObj, resultObj) { |
||||
t.Errorf("parseTopics() = %v, want %v", createObj, resultObj) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,76 @@ |
||||
// Copyright 2016 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 bind |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// WaitMined waits for tx to be mined on the blockchain.
|
||||
// It stops waiting when the context is canceled.
|
||||
func WaitMined(ctx context.Context, b DeployBackend, tx *types.Transaction) (*types.Receipt, error) { |
||||
queryTicker := time.NewTicker(time.Second) |
||||
defer queryTicker.Stop() |
||||
|
||||
logger := log.New("hash", tx.Hash()) |
||||
for { |
||||
receipt, err := b.TransactionReceipt(ctx, tx.Hash()) |
||||
if receipt != nil { |
||||
return receipt, nil |
||||
} |
||||
if err != nil { |
||||
logger.Trace("Receipt retrieval failed", "err", err) |
||||
} else { |
||||
logger.Trace("Transaction not yet mined") |
||||
} |
||||
// Wait for the next round.
|
||||
select { |
||||
case <-ctx.Done(): |
||||
return nil, ctx.Err() |
||||
case <-queryTicker.C: |
||||
} |
||||
} |
||||
} |
||||
|
||||
// WaitDeployed waits for a contract deployment transaction and returns the on-chain
|
||||
// contract address when it is mined. It stops waiting when ctx is canceled.
|
||||
func WaitDeployed(ctx context.Context, b DeployBackend, tx *types.Transaction) (common.Address, error) { |
||||
if tx.To() != nil { |
||||
return common.Address{}, fmt.Errorf("tx is not contract creation") |
||||
} |
||||
receipt, err := WaitMined(ctx, b, tx) |
||||
if err != nil { |
||||
return common.Address{}, err |
||||
} |
||||
if receipt.ContractAddress == (common.Address{}) { |
||||
return common.Address{}, fmt.Errorf("zero address") |
||||
} |
||||
// Check that code has indeed been deployed at the address.
|
||||
// This matters on pre-Homestead chains: OOG in the constructor
|
||||
// could leave an empty account behind.
|
||||
code, err := b.CodeAt(ctx, receipt.ContractAddress, nil) |
||||
if err == nil && len(code) == 0 { |
||||
err = ErrNoCodeAfterDeploy |
||||
} |
||||
return receipt.ContractAddress, err |
||||
} |
@ -0,0 +1,94 @@ |
||||
// Copyright 2016 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 bind_test |
||||
|
||||
import ( |
||||
"context" |
||||
"math/big" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
var testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") |
||||
|
||||
var waitDeployedTests = map[string]struct { |
||||
code string |
||||
gas uint64 |
||||
wantAddress common.Address |
||||
wantErr error |
||||
}{ |
||||
"successful deploy": { |
||||
code: `6060604052600a8060106000396000f360606040526008565b00`, |
||||
gas: 3000000, |
||||
wantAddress: common.HexToAddress("0x3a220f351252089d385b29beca14e27f204c296a"), |
||||
}, |
||||
"empty code": { |
||||
code: ``, |
||||
gas: 300000, |
||||
wantErr: bind.ErrNoCodeAfterDeploy, |
||||
wantAddress: common.HexToAddress("0x3a220f351252089d385b29beca14e27f204c296a"), |
||||
}, |
||||
} |
||||
|
||||
func TestWaitDeployed(t *testing.T) { |
||||
for name, test := range waitDeployedTests { |
||||
backend := backends.NewSimulatedBackend( |
||||
core.GenesisAlloc{ |
||||
crypto.PubkeyToAddress(testKey.PublicKey): {Balance: big.NewInt(10000000000)}, |
||||
}, 10000000, |
||||
) |
||||
|
||||
// Create the transaction.
|
||||
tx := types.NewContractCreation(0, big.NewInt(0), test.gas, big.NewInt(1), common.FromHex(test.code)) |
||||
tx, _ = types.SignTx(tx, types.HomesteadSigner{}, testKey) |
||||
|
||||
// Wait for it to get mined in the background.
|
||||
var ( |
||||
err error |
||||
address common.Address |
||||
mined = make(chan struct{}) |
||||
ctx = context.Background() |
||||
) |
||||
go func() { |
||||
address, err = bind.WaitDeployed(ctx, backend, tx) |
||||
close(mined) |
||||
}() |
||||
|
||||
// Send and mine the transaction.
|
||||
backend.SendTransaction(ctx, tx) |
||||
backend.Commit() |
||||
|
||||
select { |
||||
case <-mined: |
||||
if err != test.wantErr { |
||||
t.Errorf("test %q: error mismatch: got %q, want %q", name, err, test.wantErr) |
||||
} |
||||
if address != test.wantAddress { |
||||
t.Errorf("test %q: unexpected contract address %s", name, address.Hex()) |
||||
} |
||||
case <-time.After(2 * time.Second): |
||||
t.Errorf("test %q: timeout", name) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
// Copyright 2015 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 abi implements the Ethereum ABI (Application Binary
|
||||
// Interface).
|
||||
//
|
||||
// The Ethereum ABI is strongly typed, known at compile time
|
||||
// and static. This ABI will handle basic type casting; unsigned
|
||||
// to signed and visa versa. It does not handle slice casting such
|
||||
// as unsigned slice to signed slice. Bit size type casting is also
|
||||
// handled. ints with a bit size of 32 will be properly cast to int256,
|
||||
// etc.
|
||||
package abi |
@ -0,0 +1,84 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"reflect" |
||||
) |
||||
|
||||
var ( |
||||
errBadBool = errors.New("abi: improperly encoded boolean value") |
||||
) |
||||
|
||||
// formatSliceString formats the reflection kind with the given slice size
|
||||
// and returns a formatted string representation.
|
||||
func formatSliceString(kind reflect.Kind, sliceSize int) string { |
||||
if sliceSize == -1 { |
||||
return fmt.Sprintf("[]%v", kind) |
||||
} |
||||
return fmt.Sprintf("[%d]%v", sliceSize, kind) |
||||
} |
||||
|
||||
// sliceTypeCheck checks that the given slice can by assigned to the reflection
|
||||
// type in t.
|
||||
func sliceTypeCheck(t Type, val reflect.Value) error { |
||||
if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { |
||||
return typeErr(formatSliceString(t.Kind, t.Size), val.Type()) |
||||
} |
||||
|
||||
if t.T == ArrayTy && val.Len() != t.Size { |
||||
return typeErr(formatSliceString(t.Elem.Kind, t.Size), formatSliceString(val.Type().Elem().Kind(), val.Len())) |
||||
} |
||||
|
||||
if t.Elem.T == SliceTy { |
||||
if val.Len() > 0 { |
||||
return sliceTypeCheck(*t.Elem, val.Index(0)) |
||||
} |
||||
} else if t.Elem.T == ArrayTy { |
||||
return sliceTypeCheck(*t.Elem, val.Index(0)) |
||||
} |
||||
|
||||
if elemKind := val.Type().Elem().Kind(); elemKind != t.Elem.Kind { |
||||
return typeErr(formatSliceString(t.Elem.Kind, t.Size), val.Type()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// typeCheck checks that the given reflection value can be assigned to the reflection
|
||||
// type in t.
|
||||
func typeCheck(t Type, value reflect.Value) error { |
||||
if t.T == SliceTy || t.T == ArrayTy { |
||||
return sliceTypeCheck(t, value) |
||||
} |
||||
|
||||
// Check base type validity. Element types will be checked later on.
|
||||
if t.Kind != value.Kind() { |
||||
return typeErr(t.Kind, value.Kind()) |
||||
} else if t.T == FixedBytesTy && t.Size != value.Len() { |
||||
return typeErr(t.Type, value.Type()) |
||||
} else { |
||||
return nil |
||||
} |
||||
|
||||
} |
||||
|
||||
// typeErr returns a formatted type casting error.
|
||||
func typeErr(expected, got interface{}) error { |
||||
return fmt.Errorf("abi: cannot use %v as type %v as argument", got, expected) |
||||
} |
@ -0,0 +1,57 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// Event is an event potentially triggered by the EVM's LOG mechanism. The Event
|
||||
// holds type information (inputs) about the yielded output. Anonymous events
|
||||
// don't get the signature canonical representation as the first LOG topic.
|
||||
type Event struct { |
||||
Name string |
||||
Anonymous bool |
||||
Inputs Arguments |
||||
} |
||||
|
||||
func (e Event) String() string { |
||||
inputs := make([]string, len(e.Inputs)) |
||||
for i, input := range e.Inputs { |
||||
inputs[i] = fmt.Sprintf("%v %v", input.Type, input.Name) |
||||
if input.Indexed { |
||||
inputs[i] = fmt.Sprintf("%v indexed %v", input.Type, input.Name) |
||||
} |
||||
} |
||||
return fmt.Sprintf("event %v(%v)", e.Name, strings.Join(inputs, ", ")) |
||||
} |
||||
|
||||
// Id returns the canonical representation of the event's signature used by the
|
||||
// abi definition to identify event names and types.
|
||||
func (e Event) Id() common.Hash { |
||||
types := make([]string, len(e.Inputs)) |
||||
i := 0 |
||||
for _, input := range e.Inputs { |
||||
types[i] = input.Type.String() |
||||
i++ |
||||
} |
||||
return common.BytesToHash(crypto.Keccak256([]byte(fmt.Sprintf("%v(%v)", e.Name, strings.Join(types, ","))))) |
||||
} |
@ -0,0 +1,428 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"math/big" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var jsonEventTransfer = []byte(`{ |
||||
"anonymous": false, |
||||
"inputs": [ |
||||
{ |
||||
"indexed": true, "name": "from", "type": "address" |
||||
}, { |
||||
"indexed": true, "name": "to", "type": "address" |
||||
}, { |
||||
"indexed": false, "name": "value", "type": "uint256" |
||||
}], |
||||
"name": "Transfer", |
||||
"type": "event" |
||||
}`) |
||||
|
||||
var jsonEventPledge = []byte(`{ |
||||
"anonymous": false, |
||||
"inputs": [{ |
||||
"indexed": false, "name": "who", "type": "address" |
||||
}, { |
||||
"indexed": false, "name": "wad", "type": "uint128" |
||||
}, { |
||||
"indexed": false, "name": "currency", "type": "bytes3" |
||||
}], |
||||
"name": "Pledge", |
||||
"type": "event" |
||||
}`) |
||||
|
||||
var jsonEventMixedCase = []byte(`{ |
||||
"anonymous": false, |
||||
"inputs": [{ |
||||
"indexed": false, "name": "value", "type": "uint256" |
||||
}, { |
||||
"indexed": false, "name": "_value", "type": "uint256" |
||||
}, { |
||||
"indexed": false, "name": "Value", "type": "uint256" |
||||
}], |
||||
"name": "MixedCase", |
||||
"type": "event" |
||||
}`) |
||||
|
||||
// 1000000
|
||||
var transferData1 = "00000000000000000000000000000000000000000000000000000000000f4240" |
||||
|
||||
// "0x00Ce0d46d924CC8437c806721496599FC3FFA268", 2218516807680, "usd"
|
||||
var pledgeData1 = "00000000000000000000000000ce0d46d924cc8437c806721496599fc3ffa2680000000000000000000000000000000000000000000000000000020489e800007573640000000000000000000000000000000000000000000000000000000000" |
||||
|
||||
// 1000000,2218516807680,1000001
|
||||
var mixedCaseData1 = "00000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000020489e8000000000000000000000000000000000000000000000000000000000000000f4241" |
||||
|
||||
func TestEventId(t *testing.T) { |
||||
var table = []struct { |
||||
definition string |
||||
expectations map[string]common.Hash |
||||
}{ |
||||
{ |
||||
definition: `[ |
||||
{ "type" : "event", "name" : "Balance", "inputs": [{ "name" : "in", "type": "uint256" }] }, |
||||
{ "type" : "event", "name" : "Check", "inputs": [{ "name" : "t", "type": "address" }, { "name": "b", "type": "uint256" }] } |
||||
]`, |
||||
expectations: map[string]common.Hash{ |
||||
"Balance": crypto.Keccak256Hash([]byte("Balance(uint256)")), |
||||
"Check": crypto.Keccak256Hash([]byte("Check(address,uint256)")), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range table { |
||||
abi, err := JSON(strings.NewReader(test.definition)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for name, event := range abi.Events { |
||||
if event.Id() != test.expectations[name] { |
||||
t.Errorf("expected id to be %x, got %x", test.expectations[name], event.Id()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestEventString(t *testing.T) { |
||||
var table = []struct { |
||||
definition string |
||||
expectations map[string]string |
||||
}{ |
||||
{ |
||||
definition: `[ |
||||
{ "type" : "event", "name" : "Balance", "inputs": [{ "name" : "in", "type": "uint256" }] }, |
||||
{ "type" : "event", "name" : "Check", "inputs": [{ "name" : "t", "type": "address" }, { "name": "b", "type": "uint256" }] }, |
||||
{ "type" : "event", "name" : "Transfer", "inputs": [{ "name": "from", "type": "address", "indexed": true }, { "name": "to", "type": "address", "indexed": true }, { "name": "value", "type": "uint256" }] } |
||||
]`, |
||||
expectations: map[string]string{ |
||||
"Balance": "event Balance(uint256 in)", |
||||
"Check": "event Check(address t, uint256 b)", |
||||
"Transfer": "event Transfer(address indexed from, address indexed to, uint256 value)", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range table { |
||||
abi, err := JSON(strings.NewReader(test.definition)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for name, event := range abi.Events { |
||||
if event.String() != test.expectations[name] { |
||||
t.Errorf("expected string to be %s, got %s", test.expectations[name], event.String()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TestEventMultiValueWithArrayUnpack verifies that array fields will be counted after parsing array.
|
||||
func TestEventMultiValueWithArrayUnpack(t *testing.T) { |
||||
definition := `[{"name": "test", "type": "event", "inputs": [{"indexed": false, "name":"value1", "type":"uint8[2]"},{"indexed": false, "name":"value2", "type":"uint8"}]}]` |
||||
type testStruct struct { |
||||
Value1 [2]uint8 |
||||
Value2 uint8 |
||||
} |
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
require.NoError(t, err) |
||||
var b bytes.Buffer |
||||
var i uint8 = 1 |
||||
for ; i <= 3; i++ { |
||||
b.Write(packNum(reflect.ValueOf(i))) |
||||
} |
||||
var rst testStruct |
||||
require.NoError(t, abi.Unpack(&rst, "test", b.Bytes())) |
||||
require.Equal(t, [2]uint8{1, 2}, rst.Value1) |
||||
require.Equal(t, uint8(3), rst.Value2) |
||||
} |
||||
|
||||
func TestEventTupleUnpack(t *testing.T) { |
||||
|
||||
type EventTransfer struct { |
||||
Value *big.Int |
||||
} |
||||
|
||||
type EventTransferWithTag struct { |
||||
// this is valid because `value` is not exportable,
|
||||
// so value is only unmarshalled into `Value1`.
|
||||
value *big.Int |
||||
Value1 *big.Int `abi:"value"` |
||||
} |
||||
|
||||
type BadEventTransferWithSameFieldAndTag struct { |
||||
Value *big.Int |
||||
Value1 *big.Int `abi:"value"` |
||||
} |
||||
|
||||
type BadEventTransferWithDuplicatedTag struct { |
||||
Value1 *big.Int `abi:"value"` |
||||
Value2 *big.Int `abi:"value"` |
||||
} |
||||
|
||||
type BadEventTransferWithEmptyTag struct { |
||||
Value *big.Int `abi:""` |
||||
} |
||||
|
||||
type EventPledge struct { |
||||
Who common.Address |
||||
Wad *big.Int |
||||
Currency [3]byte |
||||
} |
||||
|
||||
type BadEventPledge struct { |
||||
Who string |
||||
Wad int |
||||
Currency [3]byte |
||||
} |
||||
|
||||
type EventMixedCase struct { |
||||
Value1 *big.Int `abi:"value"` |
||||
Value2 *big.Int `abi:"_value"` |
||||
Value3 *big.Int `abi:"Value"` |
||||
} |
||||
|
||||
bigint := new(big.Int) |
||||
bigintExpected := big.NewInt(1000000) |
||||
bigintExpected2 := big.NewInt(2218516807680) |
||||
bigintExpected3 := big.NewInt(1000001) |
||||
addr := common.HexToAddress("0x00Ce0d46d924CC8437c806721496599FC3FFA268") |
||||
var testCases = []struct { |
||||
data string |
||||
dest interface{} |
||||
expected interface{} |
||||
jsonLog []byte |
||||
error string |
||||
name string |
||||
}{{ |
||||
transferData1, |
||||
&EventTransfer{}, |
||||
&EventTransfer{Value: bigintExpected}, |
||||
jsonEventTransfer, |
||||
"", |
||||
"Can unpack ERC20 Transfer event into structure", |
||||
}, { |
||||
transferData1, |
||||
&[]interface{}{&bigint}, |
||||
&[]interface{}{&bigintExpected}, |
||||
jsonEventTransfer, |
||||
"", |
||||
"Can unpack ERC20 Transfer event into slice", |
||||
}, { |
||||
transferData1, |
||||
&EventTransferWithTag{}, |
||||
&EventTransferWithTag{Value1: bigintExpected}, |
||||
jsonEventTransfer, |
||||
"", |
||||
"Can unpack ERC20 Transfer event into structure with abi: tag", |
||||
}, { |
||||
transferData1, |
||||
&BadEventTransferWithDuplicatedTag{}, |
||||
&BadEventTransferWithDuplicatedTag{}, |
||||
jsonEventTransfer, |
||||
"struct: abi tag in 'Value2' already mapped", |
||||
"Can not unpack ERC20 Transfer event with duplicated abi tag", |
||||
}, { |
||||
transferData1, |
||||
&BadEventTransferWithSameFieldAndTag{}, |
||||
&BadEventTransferWithSameFieldAndTag{}, |
||||
jsonEventTransfer, |
||||
"abi: multiple variables maps to the same abi field 'value'", |
||||
"Can not unpack ERC20 Transfer event with a field and a tag mapping to the same abi variable", |
||||
}, { |
||||
transferData1, |
||||
&BadEventTransferWithEmptyTag{}, |
||||
&BadEventTransferWithEmptyTag{}, |
||||
jsonEventTransfer, |
||||
"struct: abi tag in 'Value' is empty", |
||||
"Can not unpack ERC20 Transfer event with an empty tag", |
||||
}, { |
||||
pledgeData1, |
||||
&EventPledge{}, |
||||
&EventPledge{ |
||||
addr, |
||||
bigintExpected2, |
||||
[3]byte{'u', 's', 'd'}}, |
||||
jsonEventPledge, |
||||
"", |
||||
"Can unpack Pledge event into structure", |
||||
}, { |
||||
pledgeData1, |
||||
&[]interface{}{&common.Address{}, &bigint, &[3]byte{}}, |
||||
&[]interface{}{ |
||||
&addr, |
||||
&bigintExpected2, |
||||
&[3]byte{'u', 's', 'd'}}, |
||||
jsonEventPledge, |
||||
"", |
||||
"Can unpack Pledge event into slice", |
||||
}, { |
||||
pledgeData1, |
||||
&[3]interface{}{&common.Address{}, &bigint, &[3]byte{}}, |
||||
&[3]interface{}{ |
||||
&addr, |
||||
&bigintExpected2, |
||||
&[3]byte{'u', 's', 'd'}}, |
||||
jsonEventPledge, |
||||
"", |
||||
"Can unpack Pledge event into an array", |
||||
}, { |
||||
pledgeData1, |
||||
&[]interface{}{new(int), 0, 0}, |
||||
&[]interface{}{}, |
||||
jsonEventPledge, |
||||
"abi: cannot unmarshal common.Address in to int", |
||||
"Can not unpack Pledge event into slice with wrong types", |
||||
}, { |
||||
pledgeData1, |
||||
&BadEventPledge{}, |
||||
&BadEventPledge{}, |
||||
jsonEventPledge, |
||||
"abi: cannot unmarshal common.Address in to string", |
||||
"Can not unpack Pledge event into struct with wrong filed types", |
||||
}, { |
||||
pledgeData1, |
||||
&[]interface{}{common.Address{}, new(big.Int)}, |
||||
&[]interface{}{}, |
||||
jsonEventPledge, |
||||
"abi: insufficient number of elements in the list/array for unpack, want 3, got 2", |
||||
"Can not unpack Pledge event into too short slice", |
||||
}, { |
||||
pledgeData1, |
||||
new(map[string]interface{}), |
||||
&[]interface{}{}, |
||||
jsonEventPledge, |
||||
"abi: cannot unmarshal tuple into map[string]interface {}", |
||||
"Can not unpack Pledge event into map", |
||||
}, { |
||||
mixedCaseData1, |
||||
&EventMixedCase{}, |
||||
&EventMixedCase{Value1: bigintExpected, Value2: bigintExpected2, Value3: bigintExpected3}, |
||||
jsonEventMixedCase, |
||||
"", |
||||
"Can unpack abi variables with mixed case", |
||||
}} |
||||
|
||||
for _, tc := range testCases { |
||||
assert := assert.New(t) |
||||
tc := tc |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
err := unpackTestEventData(tc.dest, tc.data, tc.jsonLog, assert) |
||||
if tc.error == "" { |
||||
assert.Nil(err, "Should be able to unpack event data.") |
||||
assert.Equal(tc.expected, tc.dest, tc.name) |
||||
} else { |
||||
assert.EqualError(err, tc.error, tc.name) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func unpackTestEventData(dest interface{}, hexData string, jsonEvent []byte, assert *assert.Assertions) error { |
||||
data, err := hex.DecodeString(hexData) |
||||
assert.NoError(err, "Hex data should be a correct hex-string") |
||||
var e Event |
||||
assert.NoError(json.Unmarshal(jsonEvent, &e), "Should be able to unmarshal event ABI") |
||||
a := ABI{Events: map[string]Event{"e": e}} |
||||
return a.Unpack(dest, "e", data) |
||||
} |
||||
|
||||
/* |
||||
Taken from |
||||
https://github.com/ethereum/go-ethereum/pull/15568
|
||||
*/ |
||||
|
||||
type testResult struct { |
||||
Values [2]*big.Int |
||||
Value1 *big.Int |
||||
Value2 *big.Int |
||||
} |
||||
|
||||
type testCase struct { |
||||
definition string |
||||
want testResult |
||||
} |
||||
|
||||
func (tc testCase) encoded(intType, arrayType Type) []byte { |
||||
var b bytes.Buffer |
||||
if tc.want.Value1 != nil { |
||||
val, _ := intType.pack(reflect.ValueOf(tc.want.Value1)) |
||||
b.Write(val) |
||||
} |
||||
|
||||
if !reflect.DeepEqual(tc.want.Values, [2]*big.Int{nil, nil}) { |
||||
val, _ := arrayType.pack(reflect.ValueOf(tc.want.Values)) |
||||
b.Write(val) |
||||
} |
||||
if tc.want.Value2 != nil { |
||||
val, _ := intType.pack(reflect.ValueOf(tc.want.Value2)) |
||||
b.Write(val) |
||||
} |
||||
return b.Bytes() |
||||
} |
||||
|
||||
// TestEventUnpackIndexed verifies that indexed field will be skipped by event decoder.
|
||||
func TestEventUnpackIndexed(t *testing.T) { |
||||
definition := `[{"name": "test", "type": "event", "inputs": [{"indexed": true, "name":"value1", "type":"uint8"},{"indexed": false, "name":"value2", "type":"uint8"}]}]` |
||||
type testStruct struct { |
||||
Value1 uint8 |
||||
Value2 uint8 |
||||
} |
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
require.NoError(t, err) |
||||
var b bytes.Buffer |
||||
b.Write(packNum(reflect.ValueOf(uint8(8)))) |
||||
var rst testStruct |
||||
require.NoError(t, abi.Unpack(&rst, "test", b.Bytes())) |
||||
require.Equal(t, uint8(0), rst.Value1) |
||||
require.Equal(t, uint8(8), rst.Value2) |
||||
} |
||||
|
||||
// TestEventIndexedWithArrayUnpack verifies that decoder will not overlow when static array is indexed input.
|
||||
func TestEventIndexedWithArrayUnpack(t *testing.T) { |
||||
definition := `[{"name": "test", "type": "event", "inputs": [{"indexed": true, "name":"value1", "type":"uint8[2]"},{"indexed": false, "name":"value2", "type":"string"}]}]` |
||||
type testStruct struct { |
||||
Value1 [2]uint8 |
||||
Value2 string |
||||
} |
||||
abi, err := JSON(strings.NewReader(definition)) |
||||
require.NoError(t, err) |
||||
var b bytes.Buffer |
||||
stringOut := "abc" |
||||
// number of fields that will be encoded * 32
|
||||
b.Write(packNum(reflect.ValueOf(32))) |
||||
b.Write(packNum(reflect.ValueOf(len(stringOut)))) |
||||
b.Write(common.RightPadBytes([]byte(stringOut), 32)) |
||||
|
||||
var rst testStruct |
||||
require.NoError(t, abi.Unpack(&rst, "test", b.Bytes())) |
||||
require.Equal(t, [2]uint8{0, 0}, rst.Value1) |
||||
require.Equal(t, stringOut, rst.Value2) |
||||
} |
@ -0,0 +1,77 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// Method represents a callable given a `Name` and whether the method is a constant.
|
||||
// If the method is `Const` no transaction needs to be created for this
|
||||
// particular Method call. It can easily be simulated using a local VM.
|
||||
// For example a `Balance()` method only needs to retrieve something
|
||||
// from the storage and therefor requires no Tx to be send to the
|
||||
// network. A method such as `Transact` does require a Tx and thus will
|
||||
// be flagged `true`.
|
||||
// Input specifies the required input parameters for this gives method.
|
||||
type Method struct { |
||||
Name string |
||||
Const bool |
||||
Inputs Arguments |
||||
Outputs Arguments |
||||
} |
||||
|
||||
// Sig returns the methods string signature according to the ABI spec.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// function foo(uint32 a, int b) = "foo(uint32,int256)"
|
||||
//
|
||||
// Please note that "int" is substitute for its canonical representation "int256"
|
||||
func (method Method) Sig() string { |
||||
types := make([]string, len(method.Inputs)) |
||||
for i, input := range method.Inputs { |
||||
types[i] = input.Type.String() |
||||
} |
||||
return fmt.Sprintf("%v(%v)", method.Name, strings.Join(types, ",")) |
||||
} |
||||
|
||||
func (method Method) String() string { |
||||
inputs := make([]string, len(method.Inputs)) |
||||
for i, input := range method.Inputs { |
||||
inputs[i] = fmt.Sprintf("%v %v", input.Type, input.Name) |
||||
} |
||||
outputs := make([]string, len(method.Outputs)) |
||||
for i, output := range method.Outputs { |
||||
outputs[i] = output.Type.String() |
||||
if len(output.Name) > 0 { |
||||
outputs[i] += fmt.Sprintf(" %v", output.Name) |
||||
} |
||||
} |
||||
constant := "" |
||||
if method.Const { |
||||
constant = "constant " |
||||
} |
||||
return fmt.Sprintf("function %v(%v) %sreturns(%v)", method.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", ")) |
||||
} |
||||
|
||||
func (method Method) Id() []byte { |
||||
return crypto.Keccak256([]byte(method.Sig()))[:4] |
||||
} |
@ -0,0 +1,61 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
const methoddata = ` |
||||
[ |
||||
{ "type" : "function", "name" : "balance", "constant" : true }, |
||||
{ "type" : "function", "name" : "send", "constant" : false, "inputs" : [ { "name" : "amount", "type" : "uint256" } ] }, |
||||
{ "type" : "function", "name" : "transfer", "constant" : false, "inputs" : [ { "name" : "from", "type" : "address" }, { "name" : "to", "type" : "address" }, { "name" : "value", "type" : "uint256" } ], "outputs" : [ { "name" : "success", "type" : "bool" } ] } |
||||
]` |
||||
|
||||
func TestMethodString(t *testing.T) { |
||||
var table = []struct { |
||||
method string |
||||
expectation string |
||||
}{ |
||||
{ |
||||
method: "balance", |
||||
expectation: "function balance() constant returns()", |
||||
}, |
||||
{ |
||||
method: "send", |
||||
expectation: "function send(uint256 amount) returns()", |
||||
}, |
||||
{ |
||||
method: "transfer", |
||||
expectation: "function transfer(address from, address to, uint256 value) returns(bool success)", |
||||
}, |
||||
} |
||||
|
||||
abi, err := JSON(strings.NewReader(methoddata)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for _, test := range table { |
||||
got := abi.Methods[test.method].String() |
||||
if got != test.expectation { |
||||
t.Errorf("expected string to be %s, got %s", test.expectation, got) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
) |
||||
|
||||
var ( |
||||
bigT = reflect.TypeOf(&big.Int{}) |
||||
derefbigT = reflect.TypeOf(big.Int{}) |
||||
uint8T = reflect.TypeOf(uint8(0)) |
||||
uint16T = reflect.TypeOf(uint16(0)) |
||||
uint32T = reflect.TypeOf(uint32(0)) |
||||
uint64T = reflect.TypeOf(uint64(0)) |
||||
int8T = reflect.TypeOf(int8(0)) |
||||
int16T = reflect.TypeOf(int16(0)) |
||||
int32T = reflect.TypeOf(int32(0)) |
||||
int64T = reflect.TypeOf(int64(0)) |
||||
addressT = reflect.TypeOf(common.Address{}) |
||||
) |
||||
|
||||
// U256 converts a big Int into a 256bit EVM number.
|
||||
func U256(n *big.Int) []byte { |
||||
return math.PaddedBigBytes(math.U256(n), 32) |
||||
} |
@ -0,0 +1,33 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"bytes" |
||||
"math/big" |
||||
"testing" |
||||
) |
||||
|
||||
func TestNumberTypes(t *testing.T) { |
||||
ubytes := make([]byte, 32) |
||||
ubytes[31] = 1 |
||||
|
||||
unsigned := U256(big.NewInt(1)) |
||||
if !bytes.Equal(unsigned, ubytes) { |
||||
t.Errorf("expected %x got %x", ubytes, unsigned) |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
) |
||||
|
||||
// packBytesSlice packs the given bytes as [L, V] as the canonical representation
|
||||
// bytes slice
|
||||
func packBytesSlice(bytes []byte, l int) []byte { |
||||
len := packNum(reflect.ValueOf(l)) |
||||
return append(len, common.RightPadBytes(bytes, (l+31)/32*32)...) |
||||
} |
||||
|
||||
// packElement packs the given reflect value according to the abi specification in
|
||||
// t.
|
||||
func packElement(t Type, reflectValue reflect.Value) []byte { |
||||
switch t.T { |
||||
case IntTy, UintTy: |
||||
return packNum(reflectValue) |
||||
case StringTy: |
||||
return packBytesSlice([]byte(reflectValue.String()), reflectValue.Len()) |
||||
case AddressTy: |
||||
if reflectValue.Kind() == reflect.Array { |
||||
reflectValue = mustArrayToByteSlice(reflectValue) |
||||
} |
||||
|
||||
return common.LeftPadBytes(reflectValue.Bytes(), 32) |
||||
case BoolTy: |
||||
if reflectValue.Bool() { |
||||
return math.PaddedBigBytes(common.Big1, 32) |
||||
} |
||||
return math.PaddedBigBytes(common.Big0, 32) |
||||
case BytesTy: |
||||
if reflectValue.Kind() == reflect.Array { |
||||
reflectValue = mustArrayToByteSlice(reflectValue) |
||||
} |
||||
return packBytesSlice(reflectValue.Bytes(), reflectValue.Len()) |
||||
case FixedBytesTy, FunctionTy: |
||||
if reflectValue.Kind() == reflect.Array { |
||||
reflectValue = mustArrayToByteSlice(reflectValue) |
||||
} |
||||
return common.RightPadBytes(reflectValue.Bytes(), 32) |
||||
default: |
||||
panic("abi: fatal error") |
||||
} |
||||
} |
||||
|
||||
// packNum packs the given number (using the reflect value) and will cast it to appropriate number representation
|
||||
func packNum(value reflect.Value) []byte { |
||||
switch kind := value.Kind(); kind { |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
return U256(new(big.Int).SetUint64(value.Uint())) |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return U256(big.NewInt(value.Int())) |
||||
case reflect.Ptr: |
||||
return U256(value.Interface().(*big.Int)) |
||||
default: |
||||
panic("abi: fatal error") |
||||
} |
||||
|
||||
} |
@ -0,0 +1,783 @@ |
||||
// 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 abi |
||||
|
||||
import ( |
||||
"bytes" |
||||
"math" |
||||
"math/big" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
func TestPack(t *testing.T) { |
||||
for i, test := range []struct { |
||||
typ string |
||||
components []ArgumentMarshaling |
||||
input interface{} |
||||
output []byte |
||||
}{ |
||||
{ |
||||
"uint8", |
||||
nil, |
||||
uint8(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint8[]", |
||||
nil, |
||||
[]uint8{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint16", |
||||
nil, |
||||
uint16(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint16[]", |
||||
nil, |
||||
[]uint16{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint32", |
||||
nil, |
||||
uint32(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint32[]", |
||||
nil, |
||||
[]uint32{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint64", |
||||
nil, |
||||
uint64(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint64[]", |
||||
nil, |
||||
[]uint64{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint256", |
||||
nil, |
||||
big.NewInt(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"uint256[]", |
||||
nil, |
||||
[]*big.Int{big.NewInt(1), big.NewInt(2)}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int8", |
||||
nil, |
||||
int8(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int8[]", |
||||
nil, |
||||
[]int8{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int16", |
||||
nil, |
||||
int16(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int16[]", |
||||
nil, |
||||
[]int16{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int32", |
||||
nil, |
||||
int32(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int32[]", |
||||
nil, |
||||
[]int32{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int64", |
||||
nil, |
||||
int64(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int64[]", |
||||
nil, |
||||
[]int64{1, 2}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int256", |
||||
nil, |
||||
big.NewInt(2), |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"int256[]", |
||||
nil, |
||||
[]*big.Int{big.NewInt(1), big.NewInt(2)}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002"), |
||||
}, |
||||
{ |
||||
"bytes1", |
||||
nil, |
||||
[1]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes2", |
||||
nil, |
||||
[2]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes3", |
||||
nil, |
||||
[3]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes4", |
||||
nil, |
||||
[4]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes5", |
||||
nil, |
||||
[5]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes6", |
||||
nil, |
||||
[6]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes7", |
||||
nil, |
||||
[7]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes8", |
||||
nil, |
||||
[8]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes9", |
||||
nil, |
||||
[9]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes10", |
||||
nil, |
||||
[10]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes11", |
||||
nil, |
||||
[11]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes12", |
||||
nil, |
||||
[12]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes13", |
||||
nil, |
||||
[13]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes14", |
||||
nil, |
||||
[14]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes15", |
||||
nil, |
||||
[15]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes16", |
||||
nil, |
||||
[16]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes17", |
||||
nil, |
||||
[17]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes18", |
||||
nil, |
||||
[18]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes19", |
||||
nil, |
||||
[19]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes20", |
||||
nil, |
||||
[20]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes21", |
||||
nil, |
||||
[21]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes22", |
||||
nil, |
||||
[22]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes23", |
||||
nil, |
||||
[23]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes24", |
||||
nil, |
||||
[24]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes25", |
||||
nil, |
||||
[25]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes26", |
||||
nil, |
||||
[26]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes27", |
||||
nil, |
||||
[27]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes28", |
||||
nil, |
||||
[28]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes29", |
||||
nil, |
||||
[29]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes30", |
||||
nil, |
||||
[30]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes31", |
||||
nil, |
||||
[31]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes32", |
||||
nil, |
||||
[32]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"uint32[2][3][4]", |
||||
nil, |
||||
[4][3][2]uint32{{{1, 2}, {3, 4}, {5, 6}}, {{7, 8}, {9, 10}, {11, 12}}, {{13, 14}, {15, 16}, {17, 18}}, {{19, 20}, {21, 22}, {23, 24}}}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001300000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000018"), |
||||
}, |
||||
{ |
||||
"address[]", |
||||
nil, |
||||
[]common.Address{{1}, {2}}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000200000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"bytes32[]", |
||||
nil, |
||||
[]common.Hash{{1}, {2}}, |
||||
common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000000201000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"function", |
||||
nil, |
||||
[24]byte{1}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"string", |
||||
nil, |
||||
"foobar", |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000006666f6f6261720000000000000000000000000000000000000000000000000000"), |
||||
}, |
||||
{ |
||||
"string[]", |
||||
nil, |
||||
[]string{"hello", "foobar"}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002" + // len(array) = 2
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // offset 64 to i = 0
|
||||
"0000000000000000000000000000000000000000000000000000000000000080" + // offset 128 to i = 1
|
||||
"0000000000000000000000000000000000000000000000000000000000000005" + // len(str[0]) = 5
|
||||
"68656c6c6f000000000000000000000000000000000000000000000000000000" + // str[0]
|
||||
"0000000000000000000000000000000000000000000000000000000000000006" + // len(str[1]) = 6
|
||||
"666f6f6261720000000000000000000000000000000000000000000000000000"), // str[1]
|
||||
}, |
||||
{ |
||||
"string[2]", |
||||
nil, |
||||
[]string{"hello", "foobar"}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000040" + // offset to i = 0
|
||||
"0000000000000000000000000000000000000000000000000000000000000080" + // offset to i = 1
|
||||
"0000000000000000000000000000000000000000000000000000000000000005" + // len(str[0]) = 5
|
||||
"68656c6c6f000000000000000000000000000000000000000000000000000000" + // str[0]
|
||||
"0000000000000000000000000000000000000000000000000000000000000006" + // len(str[1]) = 6
|
||||
"666f6f6261720000000000000000000000000000000000000000000000000000"), // str[1]
|
||||
}, |
||||
{ |
||||
"bytes32[][]", |
||||
nil, |
||||
[][]common.Hash{{{1}, {2}}, {{3}, {4}, {5}}}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002" + // len(array) = 2
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // offset 64 to i = 0
|
||||
"00000000000000000000000000000000000000000000000000000000000000a0" + // offset 160 to i = 1
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // len(array[0]) = 2
|
||||
"0100000000000000000000000000000000000000000000000000000000000000" + // array[0][0]
|
||||
"0200000000000000000000000000000000000000000000000000000000000000" + // array[0][1]
|
||||
"0000000000000000000000000000000000000000000000000000000000000003" + // len(array[1]) = 3
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // array[1][0]
|
||||
"0400000000000000000000000000000000000000000000000000000000000000" + // array[1][1]
|
||||
"0500000000000000000000000000000000000000000000000000000000000000"), // array[1][2]
|
||||
}, |
||||
|
||||
{ |
||||
"bytes32[][2]", |
||||
nil, |
||||
[][]common.Hash{{{1}, {2}}, {{3}, {4}, {5}}}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000040" + // offset 64 to i = 0
|
||||
"00000000000000000000000000000000000000000000000000000000000000a0" + // offset 160 to i = 1
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // len(array[0]) = 2
|
||||
"0100000000000000000000000000000000000000000000000000000000000000" + // array[0][0]
|
||||
"0200000000000000000000000000000000000000000000000000000000000000" + // array[0][1]
|
||||
"0000000000000000000000000000000000000000000000000000000000000003" + // len(array[1]) = 3
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // array[1][0]
|
||||
"0400000000000000000000000000000000000000000000000000000000000000" + // array[1][1]
|
||||
"0500000000000000000000000000000000000000000000000000000000000000"), // array[1][2]
|
||||
}, |
||||
|
||||
{ |
||||
"bytes32[3][2]", |
||||
nil, |
||||
[][]common.Hash{{{1}, {2}, {3}}, {{3}, {4}, {5}}}, |
||||
common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000" + // array[0][0]
|
||||
"0200000000000000000000000000000000000000000000000000000000000000" + // array[0][1]
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // array[0][2]
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // array[1][0]
|
||||
"0400000000000000000000000000000000000000000000000000000000000000" + // array[1][1]
|
||||
"0500000000000000000000000000000000000000000000000000000000000000"), // array[1][2]
|
||||
}, |
||||
{ |
||||
// static tuple
|
||||
"tuple", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "int64"}, |
||||
{Name: "b", Type: "int256"}, |
||||
{Name: "c", Type: "int256"}, |
||||
{Name: "d", Type: "bool"}, |
||||
{Name: "e", Type: "bytes32[3][2]"}, |
||||
}, |
||||
struct { |
||||
A int64 |
||||
B *big.Int |
||||
C *big.Int |
||||
D bool |
||||
E [][]common.Hash |
||||
}{1, big.NewInt(1), big.NewInt(-1), true, [][]common.Hash{{{1}, {2}, {3}}, {{3}, {4}, {5}}}}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001" + // struct[a]
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // struct[b]
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // struct[c]
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // struct[d]
|
||||
"0100000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[0][0]
|
||||
"0200000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[0][1]
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[0][2]
|
||||
"0300000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[1][0]
|
||||
"0400000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[1][1]
|
||||
"0500000000000000000000000000000000000000000000000000000000000000"), // struct[e] array[1][2]
|
||||
}, |
||||
{ |
||||
// dynamic tuple
|
||||
"tuple", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "string"}, |
||||
{Name: "b", Type: "int64"}, |
||||
{Name: "c", Type: "bytes"}, |
||||
{Name: "d", Type: "string[]"}, |
||||
{Name: "e", Type: "int256[]"}, |
||||
{Name: "f", Type: "address[]"}, |
||||
}, |
||||
struct { |
||||
FieldA string `abi:"a"` // Test whether abi tag works
|
||||
FieldB int64 `abi:"b"` |
||||
C []byte |
||||
D []string |
||||
E []*big.Int |
||||
F []common.Address |
||||
}{"foobar", 1, []byte{1}, []string{"foo", "bar"}, []*big.Int{big.NewInt(1), big.NewInt(-1)}, []common.Address{{1}, {2}}}, |
||||
common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000000000c0" + // struct[a] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // struct[b]
|
||||
"0000000000000000000000000000000000000000000000000000000000000100" + // struct[c] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000140" + // struct[d] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000220" + // struct[e] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000280" + // struct[f] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000006" + // struct[a] length
|
||||
"666f6f6261720000000000000000000000000000000000000000000000000000" + // struct[a] "foobar"
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // struct[c] length
|
||||
"0100000000000000000000000000000000000000000000000000000000000000" + // []byte{1}
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // struct[d] length
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // foo offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000080" + // bar offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000003" + // foo length
|
||||
"666f6f0000000000000000000000000000000000000000000000000000000000" + // foo
|
||||
"0000000000000000000000000000000000000000000000000000000000000003" + // bar offset
|
||||
"6261720000000000000000000000000000000000000000000000000000000000" + // bar
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // struct[e] length
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // 1
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // -1
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // struct[f] length
|
||||
"0000000000000000000000000100000000000000000000000000000000000000" + // common.Address{1}
|
||||
"0000000000000000000000000200000000000000000000000000000000000000"), // common.Address{2}
|
||||
}, |
||||
{ |
||||
// nested tuple
|
||||
"tuple", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "tuple", Components: []ArgumentMarshaling{{Name: "a", Type: "uint256"}, {Name: "b", Type: "uint256[]"}}}, |
||||
{Name: "b", Type: "int256[]"}, |
||||
}, |
||||
struct { |
||||
A struct { |
||||
FieldA *big.Int `abi:"a"` |
||||
B []*big.Int |
||||
} |
||||
B []*big.Int |
||||
}{ |
||||
A: struct { |
||||
FieldA *big.Int `abi:"a"` // Test whether abi tag works for nested tuple
|
||||
B []*big.Int |
||||
}{big.NewInt(1), []*big.Int{big.NewInt(1), big.NewInt(0)}}, |
||||
B: []*big.Int{big.NewInt(1), big.NewInt(0)}}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000040" + // a offset
|
||||
"00000000000000000000000000000000000000000000000000000000000000e0" + // b offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // a.a value
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // a.b offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // a.b length
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // a.b[0] value
|
||||
"0000000000000000000000000000000000000000000000000000000000000000" + // a.b[1] value
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // b length
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // b[0] value
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"), // b[1] value
|
||||
}, |
||||
{ |
||||
// tuple slice
|
||||
"tuple[]", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "int256"}, |
||||
{Name: "b", Type: "int256[]"}, |
||||
}, |
||||
[]struct { |
||||
A *big.Int |
||||
B []*big.Int |
||||
}{ |
||||
{big.NewInt(-1), []*big.Int{big.NewInt(1), big.NewInt(0)}}, |
||||
{big.NewInt(1), []*big.Int{big.NewInt(2), big.NewInt(-1)}}, |
||||
}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002" + // tuple length
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // tuple[0] offset
|
||||
"00000000000000000000000000000000000000000000000000000000000000e0" + // tuple[1] offset
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // tuple[0].A
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // tuple[0].B offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // tuple[0].B length
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[0].B[0] value
|
||||
"0000000000000000000000000000000000000000000000000000000000000000" + // tuple[0].B[1] value
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[1].A
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + // tuple[1].B offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // tuple[1].B length
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // tuple[1].B[0] value
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), // tuple[1].B[1] value
|
||||
}, |
||||
{ |
||||
// static tuple array
|
||||
"tuple[2]", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "int256"}, |
||||
{Name: "b", Type: "int256"}, |
||||
}, |
||||
[2]struct { |
||||
A *big.Int |
||||
B *big.Int |
||||
}{ |
||||
{big.NewInt(-1), big.NewInt(1)}, |
||||
{big.NewInt(1), big.NewInt(-1)}, |
||||
}, |
||||
common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // tuple[0].a
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[0].b
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[1].a
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), // tuple[1].b
|
||||
}, |
||||
{ |
||||
// dynamic tuple array
|
||||
"tuple[2]", |
||||
[]ArgumentMarshaling{ |
||||
{Name: "a", Type: "int256[]"}, |
||||
}, |
||||
[2]struct { |
||||
A []*big.Int |
||||
}{ |
||||
{[]*big.Int{big.NewInt(-1), big.NewInt(1)}}, |
||||
{[]*big.Int{big.NewInt(1), big.NewInt(-1)}}, |
||||
}, |
||||
common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000040" + // tuple[0] offset
|
||||
"00000000000000000000000000000000000000000000000000000000000000c0" + // tuple[1] offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000020" + // tuple[0].A offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // tuple[0].A length
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // tuple[0].A[0]
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[0].A[1]
|
||||
"0000000000000000000000000000000000000000000000000000000000000020" + // tuple[1].A offset
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + // tuple[1].A length
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + // tuple[1].A[0]
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), // tuple[1].A[1]
|
||||
}, |
||||
} { |
||||
typ, err := NewType(test.typ, test.components) |
||||
if err != nil { |
||||
t.Fatalf("%v failed. Unexpected parse error: %v", i, err) |
||||
} |
||||
output, err := typ.pack(reflect.ValueOf(test.input)) |
||||
if err != nil { |
||||
t.Fatalf("%v failed. Unexpected pack error: %v", i, err) |
||||
} |
||||
|
||||
if !bytes.Equal(output, test.output) { |
||||
t.Errorf("input %d for typ: %v failed. Expected bytes: '%x' Got: '%x'", i, typ.String(), test.output, output) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestMethodPack(t *testing.T) { |
||||
abi, err := JSON(strings.NewReader(jsondata2)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
sig := abi.Methods["slice"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
|
||||
packed, err := abi.Pack("slice", []uint32{1, 2}) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
var addrA, addrB = common.Address{1}, common.Address{2} |
||||
sig = abi.Methods["sliceAddress"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{32}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrA[:], 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrB[:], 32)...) |
||||
|
||||
packed, err = abi.Pack("sliceAddress", []common.Address{addrA, addrB}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
var addrC, addrD = common.Address{3}, common.Address{4} |
||||
sig = abi.Methods["sliceMultiAddress"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{64}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{160}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrA[:], 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrB[:], 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrC[:], 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrD[:], 32)...) |
||||
|
||||
packed, err = abi.Pack("sliceMultiAddress", []common.Address{addrA, addrB}, []common.Address{addrC, addrD}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
sig = abi.Methods["slice256"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
|
||||
packed, err = abi.Pack("slice256", []*big.Int{big.NewInt(1), big.NewInt(2)}) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
a := [2][2]*big.Int{{big.NewInt(1), big.NewInt(1)}, {big.NewInt(2), big.NewInt(0)}} |
||||
sig = abi.Methods["nestedArray"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0xa0}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrC[:], 32)...) |
||||
sig = append(sig, common.LeftPadBytes(addrD[:], 32)...) |
||||
packed, err = abi.Pack("nestedArray", a, []common.Address{addrC, addrD}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
sig = abi.Methods["nestedArray2"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x20}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x40}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x80}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
packed, err = abi.Pack("nestedArray2", [2][]uint8{{1}, {1}}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
|
||||
sig = abi.Methods["nestedSlice"].Id() |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x20}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x02}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0x40}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{0xa0}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{1}, 32)...) |
||||
sig = append(sig, common.LeftPadBytes([]byte{2}, 32)...) |
||||
packed, err = abi.Pack("nestedSlice", [][]uint8{{1, 2}, {1, 2}}) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(packed, sig) { |
||||
t.Errorf("expected %x got %x", sig, packed) |
||||
} |
||||
} |
||||
|
||||
func TestPackNumber(t *testing.T) { |
||||
tests := []struct { |
||||
value reflect.Value |
||||
packed []byte |
||||
}{ |
||||
// Protocol limits
|
||||
{reflect.ValueOf(0), common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000")}, |
||||
{reflect.ValueOf(1), common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001")}, |
||||
{reflect.ValueOf(-1), common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")}, |
||||
|
||||
// Type corner cases
|
||||
{reflect.ValueOf(uint8(math.MaxUint8)), common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000000000ff")}, |
||||
{reflect.ValueOf(uint16(math.MaxUint16)), common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000ffff")}, |
||||
{reflect.ValueOf(uint32(math.MaxUint32)), common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000ffffffff")}, |
||||
{reflect.ValueOf(uint64(math.MaxUint64)), common.Hex2Bytes("000000000000000000000000000000000000000000000000ffffffffffffffff")}, |
||||
|
||||
{reflect.ValueOf(int8(math.MaxInt8)), common.Hex2Bytes("000000000000000000000000000000000000000000000000000000000000007f")}, |
||||
{reflect.ValueOf(int16(math.MaxInt16)), common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000007fff")}, |
||||
{reflect.ValueOf(int32(math.MaxInt32)), common.Hex2Bytes("000000000000000000000000000000000000000000000000000000007fffffff")}, |
||||
{reflect.ValueOf(int64(math.MaxInt64)), common.Hex2Bytes("0000000000000000000000000000000000000000000000007fffffffffffffff")}, |
||||
|
||||
{reflect.ValueOf(int8(math.MinInt8)), common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80")}, |
||||
{reflect.ValueOf(int16(math.MinInt16)), common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8000")}, |
||||
{reflect.ValueOf(int32(math.MinInt32)), common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffff80000000")}, |
||||
{reflect.ValueOf(int64(math.MinInt64)), common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffff8000000000000000")}, |
||||
} |
||||
for i, tt := range tests { |
||||
packed := packNum(tt.value) |
||||
if !bytes.Equal(packed, tt.packed) { |
||||
t.Errorf("test %d: pack mismatch: have %x, want %x", i, packed, tt.packed) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,218 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
"strings" |
||||
) |
||||
|
||||
// indirect recursively dereferences the value until it either gets the value
|
||||
// or finds a big.Int
|
||||
func indirect(v reflect.Value) reflect.Value { |
||||
if v.Kind() == reflect.Ptr && v.Elem().Type() != derefbigT { |
||||
return indirect(v.Elem()) |
||||
} |
||||
return v |
||||
} |
||||
|
||||
// reflectIntKind returns the reflect using the given size and
|
||||
// unsignedness.
|
||||
func reflectIntKindAndType(unsigned bool, size int) (reflect.Kind, reflect.Type) { |
||||
switch size { |
||||
case 8: |
||||
if unsigned { |
||||
return reflect.Uint8, uint8T |
||||
} |
||||
return reflect.Int8, int8T |
||||
case 16: |
||||
if unsigned { |
||||
return reflect.Uint16, uint16T |
||||
} |
||||
return reflect.Int16, int16T |
||||
case 32: |
||||
if unsigned { |
||||
return reflect.Uint32, uint32T |
||||
} |
||||
return reflect.Int32, int32T |
||||
case 64: |
||||
if unsigned { |
||||
return reflect.Uint64, uint64T |
||||
} |
||||
return reflect.Int64, int64T |
||||
} |
||||
return reflect.Ptr, bigT |
||||
} |
||||
|
||||
// mustArrayToBytesSlice creates a new byte slice with the exact same size as value
|
||||
// and copies the bytes in value to the new slice.
|
||||
func mustArrayToByteSlice(value reflect.Value) reflect.Value { |
||||
slice := reflect.MakeSlice(reflect.TypeOf([]byte{}), value.Len(), value.Len()) |
||||
reflect.Copy(slice, value) |
||||
return slice |
||||
} |
||||
|
||||
// set attempts to assign src to dst by either setting, copying or otherwise.
|
||||
//
|
||||
// set is a bit more lenient when it comes to assignment and doesn't force an as
|
||||
// strict ruleset as bare `reflect` does.
|
||||
func set(dst, src reflect.Value) error { |
||||
dstType, srcType := dst.Type(), src.Type() |
||||
switch { |
||||
case dstType.Kind() == reflect.Interface && dst.Elem().IsValid(): |
||||
return set(dst.Elem(), src) |
||||
case dstType.Kind() == reflect.Ptr && dstType.Elem() != derefbigT: |
||||
return set(dst.Elem(), src) |
||||
case srcType.AssignableTo(dstType) && dst.CanSet(): |
||||
dst.Set(src) |
||||
case dstType.Kind() == reflect.Slice && srcType.Kind() == reflect.Slice: |
||||
return setSlice(dst, src) |
||||
default: |
||||
return fmt.Errorf("abi: cannot unmarshal %v in to %v", src.Type(), dst.Type()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// setSlice attempts to assign src to dst when slices are not assignable by default
|
||||
// e.g. src: [][]byte -> dst: [][15]byte
|
||||
func setSlice(dst, src reflect.Value) error { |
||||
slice := reflect.MakeSlice(dst.Type(), src.Len(), src.Len()) |
||||
for i := 0; i < src.Len(); i++ { |
||||
v := src.Index(i) |
||||
reflect.Copy(slice.Index(i), v) |
||||
} |
||||
|
||||
dst.Set(slice) |
||||
return nil |
||||
} |
||||
|
||||
// requireAssignable assures that `dest` is a pointer and it's not an interface.
|
||||
func requireAssignable(dst, src reflect.Value) error { |
||||
if dst.Kind() != reflect.Ptr && dst.Kind() != reflect.Interface { |
||||
return fmt.Errorf("abi: cannot unmarshal %v into %v", src.Type(), dst.Type()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// requireUnpackKind verifies preconditions for unpacking `args` into `kind`
|
||||
func requireUnpackKind(v reflect.Value, t reflect.Type, k reflect.Kind, |
||||
args Arguments) error { |
||||
|
||||
switch k { |
||||
case reflect.Struct: |
||||
case reflect.Slice, reflect.Array: |
||||
if minLen := args.LengthNonIndexed(); v.Len() < minLen { |
||||
return fmt.Errorf("abi: insufficient number of elements in the list/array for unpack, want %d, got %d", |
||||
minLen, v.Len()) |
||||
} |
||||
default: |
||||
return fmt.Errorf("abi: cannot unmarshal tuple into %v", t) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// mapArgNamesToStructFields maps a slice of argument names to struct fields.
|
||||
// first round: for each Exportable field that contains a `abi:""` tag
|
||||
// and this field name exists in the given argument name list, pair them together.
|
||||
// second round: for each argument name that has not been already linked,
|
||||
// find what variable is expected to be mapped into, if it exists and has not been
|
||||
// used, pair them.
|
||||
// Note this function assumes the given value is a struct value.
|
||||
func mapArgNamesToStructFields(argNames []string, value reflect.Value) (map[string]string, error) { |
||||
typ := value.Type() |
||||
|
||||
abi2struct := make(map[string]string) |
||||
struct2abi := make(map[string]string) |
||||
|
||||
// first round ~~~
|
||||
for i := 0; i < typ.NumField(); i++ { |
||||
structFieldName := typ.Field(i).Name |
||||
|
||||
// skip private struct fields.
|
||||
if structFieldName[:1] != strings.ToUpper(structFieldName[:1]) { |
||||
continue |
||||
} |
||||
// skip fields that have no abi:"" tag.
|
||||
var ok bool |
||||
var tagName string |
||||
if tagName, ok = typ.Field(i).Tag.Lookup("abi"); !ok { |
||||
continue |
||||
} |
||||
// check if tag is empty.
|
||||
if tagName == "" { |
||||
return nil, fmt.Errorf("struct: abi tag in '%s' is empty", structFieldName) |
||||
} |
||||
// check which argument field matches with the abi tag.
|
||||
found := false |
||||
for _, arg := range argNames { |
||||
if arg == tagName { |
||||
if abi2struct[arg] != "" { |
||||
return nil, fmt.Errorf("struct: abi tag in '%s' already mapped", structFieldName) |
||||
} |
||||
// pair them
|
||||
abi2struct[arg] = structFieldName |
||||
struct2abi[structFieldName] = arg |
||||
found = true |
||||
} |
||||
} |
||||
// check if this tag has been mapped.
|
||||
if !found { |
||||
return nil, fmt.Errorf("struct: abi tag '%s' defined but not found in abi", tagName) |
||||
} |
||||
} |
||||
|
||||
// second round ~~~
|
||||
for _, argName := range argNames { |
||||
|
||||
structFieldName := ToCamelCase(argName) |
||||
|
||||
if structFieldName == "" { |
||||
return nil, fmt.Errorf("abi: purely underscored output cannot unpack to struct") |
||||
} |
||||
|
||||
// this abi has already been paired, skip it... unless there exists another, yet unassigned
|
||||
// struct field with the same field name. If so, raise an error:
|
||||
// abi: [ { "name": "value" } ]
|
||||
// struct { Value *big.Int , Value1 *big.Int `abi:"value"`}
|
||||
if abi2struct[argName] != "" { |
||||
if abi2struct[argName] != structFieldName && |
||||
struct2abi[structFieldName] == "" && |
||||
value.FieldByName(structFieldName).IsValid() { |
||||
return nil, fmt.Errorf("abi: multiple variables maps to the same abi field '%s'", argName) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
// return an error if this struct field has already been paired.
|
||||
if struct2abi[structFieldName] != "" { |
||||
return nil, fmt.Errorf("abi: multiple outputs mapping to the same struct field '%s'", structFieldName) |
||||
} |
||||
|
||||
if value.FieldByName(structFieldName).IsValid() { |
||||
// pair them
|
||||
abi2struct[argName] = structFieldName |
||||
struct2abi[structFieldName] = argName |
||||
} else { |
||||
// not paired, but annotate as used, to detect cases like
|
||||
// abi : [ { "name": "value" }, { "name": "_value" } ]
|
||||
// struct { Value *big.Int }
|
||||
struct2abi[structFieldName] = argName |
||||
} |
||||
} |
||||
return abi2struct, nil |
||||
} |
@ -0,0 +1,191 @@ |
||||
// 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 abi |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
type reflectTest struct { |
||||
name string |
||||
args []string |
||||
struc interface{} |
||||
want map[string]string |
||||
err string |
||||
} |
||||
|
||||
var reflectTests = []reflectTest{ |
||||
{ |
||||
name: "OneToOneCorrespondance", |
||||
args: []string{"fieldA"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "MissingFieldsInStruct", |
||||
args: []string{"fieldA", "fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "MoreFieldsInStructThanArgs", |
||||
args: []string{"fieldA"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
FieldB int |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "MissingFieldInArgs", |
||||
args: []string{"fieldA"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
FieldB int `abi:"fieldB"` |
||||
}{}, |
||||
err: "struct: abi tag 'fieldB' defined but not found in abi", |
||||
}, |
||||
{ |
||||
name: "NoAbiDescriptor", |
||||
args: []string{"fieldA"}, |
||||
struc: struct { |
||||
FieldA int |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "NoArgs", |
||||
args: []string{}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
}{}, |
||||
err: "struct: abi tag 'fieldA' defined but not found in abi", |
||||
}, |
||||
{ |
||||
name: "DifferentName", |
||||
args: []string{"fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldB"` |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldB": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "DifferentName", |
||||
args: []string{"fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldB"` |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldB": "FieldA", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "MultipleFields", |
||||
args: []string{"fieldA", "fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
FieldB int `abi:"fieldB"` |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
"fieldB": "FieldB", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "MultipleFieldsABIMissing", |
||||
args: []string{"fieldA", "fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldA"` |
||||
FieldB int |
||||
}{}, |
||||
want: map[string]string{ |
||||
"fieldA": "FieldA", |
||||
"fieldB": "FieldB", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "NameConflict", |
||||
args: []string{"fieldB"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldB"` |
||||
FieldB int |
||||
}{}, |
||||
err: "abi: multiple variables maps to the same abi field 'fieldB'", |
||||
}, |
||||
{ |
||||
name: "Underscored", |
||||
args: []string{"_"}, |
||||
struc: struct { |
||||
FieldA int |
||||
}{}, |
||||
err: "abi: purely underscored output cannot unpack to struct", |
||||
}, |
||||
{ |
||||
name: "DoubleMapping", |
||||
args: []string{"fieldB", "fieldC", "fieldA"}, |
||||
struc: struct { |
||||
FieldA int `abi:"fieldC"` |
||||
FieldB int |
||||
}{}, |
||||
err: "abi: multiple outputs mapping to the same struct field 'FieldA'", |
||||
}, |
||||
{ |
||||
name: "AlreadyMapped", |
||||
args: []string{"fieldB", "fieldB"}, |
||||
struc: struct { |
||||
FieldB int `abi:"fieldB"` |
||||
}{}, |
||||
err: "struct: abi tag in 'FieldB' already mapped", |
||||
}, |
||||
} |
||||
|
||||
func TestReflectNameToStruct(t *testing.T) { |
||||
for _, test := range reflectTests { |
||||
t.Run(test.name, func(t *testing.T) { |
||||
m, err := mapArgNamesToStructFields(test.args, reflect.ValueOf(test.struc)) |
||||
if len(test.err) > 0 { |
||||
if err == nil || err.Error() != test.err { |
||||
t.Fatalf("Invalid error: expected %v, got %v", test.err, err) |
||||
} |
||||
} else { |
||||
if err != nil { |
||||
t.Fatalf("Unexpected error: %v", err) |
||||
} |
||||
for fname := range test.want { |
||||
if m[fname] != test.want[fname] { |
||||
t.Fatalf("Incorrect value for field %s: expected %v, got %v", fname, test.want[fname], m[fname]) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,353 @@ |
||||
// Copyright 2015 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 abi |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"reflect" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
// Type enumerator
|
||||
const ( |
||||
IntTy byte = iota |
||||
UintTy |
||||
BoolTy |
||||
StringTy |
||||
SliceTy |
||||
ArrayTy |
||||
TupleTy |
||||
AddressTy |
||||
FixedBytesTy |
||||
BytesTy |
||||
HashTy |
||||
FixedPointTy |
||||
FunctionTy |
||||
) |
||||
|
||||
// Type is the reflection of the supported argument type
|
||||
type Type struct { |
||||
Elem *Type |
||||
Kind reflect.Kind |
||||
Type reflect.Type |
||||
Size int |
||||
T byte // Our own type checking
|
||||
|
||||
stringKind string // holds the unparsed string for deriving signatures
|
||||
|
||||
// Tuple relative fields
|
||||
TupleElems []*Type // Type information of all tuple fields
|
||||
TupleRawNames []string // Raw field name of all tuple fields
|
||||
} |
||||
|
||||
var ( |
||||
// typeRegex parses the abi sub types
|
||||
typeRegex = regexp.MustCompile("([a-zA-Z]+)(([0-9]+)(x([0-9]+))?)?") |
||||
) |
||||
|
||||
// NewType creates a new reflection type of abi type given in t.
|
||||
func NewType(t string, components []ArgumentMarshaling) (typ Type, err error) { |
||||
// check that array brackets are equal if they exist
|
||||
if strings.Count(t, "[") != strings.Count(t, "]") { |
||||
return Type{}, fmt.Errorf("invalid arg type in abi") |
||||
} |
||||
|
||||
typ.stringKind = t |
||||
|
||||
// if there are brackets, get ready to go into slice/array mode and
|
||||
// recursively create the type
|
||||
if strings.Count(t, "[") != 0 { |
||||
i := strings.LastIndex(t, "[") |
||||
// recursively embed the type
|
||||
embeddedType, err := NewType(t[:i], components) |
||||
if err != nil { |
||||
return Type{}, err |
||||
} |
||||
// grab the last cell and create a type from there
|
||||
sliced := t[i:] |
||||
// grab the slice size with regexp
|
||||
re := regexp.MustCompile("[0-9]+") |
||||
intz := re.FindAllString(sliced, -1) |
||||
|
||||
if len(intz) == 0 { |
||||
// is a slice
|
||||
typ.T = SliceTy |
||||
typ.Kind = reflect.Slice |
||||
typ.Elem = &embeddedType |
||||
typ.Type = reflect.SliceOf(embeddedType.Type) |
||||
if embeddedType.T == TupleTy { |
||||
typ.stringKind = embeddedType.stringKind + sliced |
||||
} |
||||
} else if len(intz) == 1 { |
||||
// is a array
|
||||
typ.T = ArrayTy |
||||
typ.Kind = reflect.Array |
||||
typ.Elem = &embeddedType |
||||
typ.Size, err = strconv.Atoi(intz[0]) |
||||
if err != nil { |
||||
return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err) |
||||
} |
||||
typ.Type = reflect.ArrayOf(typ.Size, embeddedType.Type) |
||||
if embeddedType.T == TupleTy { |
||||
typ.stringKind = embeddedType.stringKind + sliced |
||||
} |
||||
} else { |
||||
return Type{}, fmt.Errorf("invalid formatting of array type") |
||||
} |
||||
return typ, err |
||||
} |
||||
// parse the type and size of the abi-type.
|
||||
matches := typeRegex.FindAllStringSubmatch(t, -1) |
||||
if len(matches) == 0 { |
||||
return Type{}, fmt.Errorf("invalid type '%v'", t) |
||||
} |
||||
parsedType := matches[0] |
||||
|
||||
// varSize is the size of the variable
|
||||
var varSize int |
||||
if len(parsedType[3]) > 0 { |
||||
var err error |
||||
varSize, err = strconv.Atoi(parsedType[2]) |
||||
if err != nil { |
||||
return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err) |
||||
} |
||||
} else { |
||||
if parsedType[0] == "uint" || parsedType[0] == "int" { |
||||
// this should fail because it means that there's something wrong with
|
||||
// the abi type (the compiler should always format it to the size...always)
|
||||
return Type{}, fmt.Errorf("unsupported arg type: %s", t) |
||||
} |
||||
} |
||||
// varType is the parsed abi type
|
||||
switch varType := parsedType[1]; varType { |
||||
case "int": |
||||
typ.Kind, typ.Type = reflectIntKindAndType(false, varSize) |
||||
typ.Size = varSize |
||||
typ.T = IntTy |
||||
case "uint": |
||||
typ.Kind, typ.Type = reflectIntKindAndType(true, varSize) |
||||
typ.Size = varSize |
||||
typ.T = UintTy |
||||
case "bool": |
||||
typ.Kind = reflect.Bool |
||||
typ.T = BoolTy |
||||
typ.Type = reflect.TypeOf(bool(false)) |
||||
case "address": |
||||
typ.Kind = reflect.Array |
||||
typ.Type = addressT |
||||
typ.Size = 20 |
||||
typ.T = AddressTy |
||||
case "string": |
||||
typ.Kind = reflect.String |
||||
typ.Type = reflect.TypeOf("") |
||||
typ.T = StringTy |
||||
case "bytes": |
||||
if varSize == 0 { |
||||
typ.T = BytesTy |
||||
typ.Kind = reflect.Slice |
||||
typ.Type = reflect.SliceOf(reflect.TypeOf(byte(0))) |
||||
} else { |
||||
typ.T = FixedBytesTy |
||||
typ.Kind = reflect.Array |
||||
typ.Size = varSize |
||||
typ.Type = reflect.ArrayOf(varSize, reflect.TypeOf(byte(0))) |
||||
} |
||||
case "tuple": |
||||
var ( |
||||
fields []reflect.StructField |
||||
elems []*Type |
||||
names []string |
||||
expression string // canonical parameter expression
|
||||
) |
||||
expression += "(" |
||||
for idx, c := range components { |
||||
cType, err := NewType(c.Type, c.Components) |
||||
if err != nil { |
||||
return Type{}, err |
||||
} |
||||
if ToCamelCase(c.Name) == "" { |
||||
return Type{}, errors.New("abi: purely anonymous or underscored field is not supported") |
||||
} |
||||
fields = append(fields, reflect.StructField{ |
||||
Name: ToCamelCase(c.Name), // reflect.StructOf will panic for any exported field.
|
||||
Type: cType.Type, |
||||
Tag: reflect.StructTag("json:\"" + c.Name + "\""), |
||||
}) |
||||
elems = append(elems, &cType) |
||||
names = append(names, c.Name) |
||||
expression += cType.stringKind |
||||
if idx != len(components)-1 { |
||||
expression += "," |
||||
} |
||||
} |
||||
expression += ")" |
||||
typ.Kind = reflect.Struct |
||||
typ.Type = reflect.StructOf(fields) |
||||
typ.TupleElems = elems |
||||
typ.TupleRawNames = names |
||||
typ.T = TupleTy |
||||
typ.stringKind = expression |
||||
case "function": |
||||
typ.Kind = reflect.Array |
||||
typ.T = FunctionTy |
||||
typ.Size = 24 |
||||
typ.Type = reflect.ArrayOf(24, reflect.TypeOf(byte(0))) |
||||
default: |
||||
return Type{}, fmt.Errorf("unsupported arg type: %s", t) |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
// String implements Stringer
|
||||
func (t Type) String() (out string) { |
||||
return t.stringKind |
||||
} |
||||
|
||||
func (t Type) pack(v reflect.Value) ([]byte, error) { |
||||
// dereference pointer first if it's a pointer
|
||||
v = indirect(v) |
||||
if err := typeCheck(t, v); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch t.T { |
||||
case SliceTy, ArrayTy: |
||||
var ret []byte |
||||
|
||||
if t.requiresLengthPrefix() { |
||||
// append length
|
||||
ret = append(ret, packNum(reflect.ValueOf(v.Len()))...) |
||||
} |
||||
|
||||
// calculate offset if any
|
||||
offset := 0 |
||||
offsetReq := isDynamicType(*t.Elem) |
||||
if offsetReq { |
||||
offset = getTypeSize(*t.Elem) * v.Len() |
||||
} |
||||
var tail []byte |
||||
for i := 0; i < v.Len(); i++ { |
||||
val, err := t.Elem.pack(v.Index(i)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if !offsetReq { |
||||
ret = append(ret, val...) |
||||
continue |
||||
} |
||||
ret = append(ret, packNum(reflect.ValueOf(offset))...) |
||||
offset += len(val) |
||||
tail = append(tail, val...) |
||||
} |
||||
return append(ret, tail...), nil |
||||
case TupleTy: |
||||
// (T1,...,Tk) for k >= 0 and any types T1, …, Tk
|
||||
// enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
|
||||
// where X = (X(1), ..., X(k)) and head and tail are defined for Ti being a static
|
||||
// type as
|
||||
// head(X(i)) = enc(X(i)) and tail(X(i)) = "" (the empty string)
|
||||
// and as
|
||||
// head(X(i)) = enc(len(head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1))))
|
||||
// tail(X(i)) = enc(X(i))
|
||||
// otherwise, i.e. if Ti is a dynamic type.
|
||||
fieldmap, err := mapArgNamesToStructFields(t.TupleRawNames, v) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Calculate prefix occupied size.
|
||||
offset := 0 |
||||
for _, elem := range t.TupleElems { |
||||
offset += getTypeSize(*elem) |
||||
} |
||||
var ret, tail []byte |
||||
for i, elem := range t.TupleElems { |
||||
field := v.FieldByName(fieldmap[t.TupleRawNames[i]]) |
||||
if !field.IsValid() { |
||||
return nil, fmt.Errorf("field %s for tuple not found in the given struct", t.TupleRawNames[i]) |
||||
} |
||||
val, err := elem.pack(field) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if isDynamicType(*elem) { |
||||
ret = append(ret, packNum(reflect.ValueOf(offset))...) |
||||
tail = append(tail, val...) |
||||
offset += len(val) |
||||
} else { |
||||
ret = append(ret, val...) |
||||
} |
||||
} |
||||
return append(ret, tail...), nil |
||||
|
||||
default: |
||||
return packElement(t, v), nil |
||||
} |
||||
} |
||||
|
||||
// requireLengthPrefix returns whether the type requires any sort of length
|
||||
// prefixing.
|
||||
func (t Type) requiresLengthPrefix() bool { |
||||
return t.T == StringTy || t.T == BytesTy || t.T == SliceTy |
||||
} |
||||
|
||||
// isDynamicType returns true if the type is dynamic.
|
||||
// The following types are called “dynamic”:
|
||||
// * bytes
|
||||
// * string
|
||||
// * T[] for any T
|
||||
// * T[k] for any dynamic T and any k >= 0
|
||||
// * (T1,...,Tk) if Ti is dynamic for some 1 <= i <= k
|
||||
func isDynamicType(t Type) bool { |
||||
if t.T == TupleTy { |
||||
for _, elem := range t.TupleElems { |
||||
if isDynamicType(*elem) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
return t.T == StringTy || t.T == BytesTy || t.T == SliceTy || (t.T == ArrayTy && isDynamicType(*t.Elem)) |
||||
} |
||||
|
||||
// getTypeSize returns the size that this type needs to occupy.
|
||||
// We distinguish static and dynamic types. Static types are encoded in-place
|
||||
// and dynamic types are encoded at a separately allocated location after the
|
||||
// current block.
|
||||
// So for a static variable, the size returned represents the size that the
|
||||
// variable actually occupies.
|
||||
// For a dynamic variable, the returned size is fixed 32 bytes, which is used
|
||||
// to store the location reference for actual value storage.
|
||||
func getTypeSize(t Type) int { |
||||
if t.T == ArrayTy && !isDynamicType(*t.Elem) { |
||||
// Recursively calculate type size if it is a nested array
|
||||
if t.Elem.T == ArrayTy { |
||||
return t.Size * getTypeSize(*t.Elem) |
||||
} |
||||
return t.Size * 32 |
||||
} else if t.T == TupleTy && !isDynamicType(t) { |
||||
total := 0 |
||||
for _, elem := range t.TupleElems { |
||||
total += getTypeSize(*elem) |
||||
} |
||||
return total |
||||
} |
||||
return 32 |
||||
} |
@ -0,0 +1,308 @@ |
||||
// Copyright 2016 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 abi |
||||
|
||||
import ( |
||||
"math/big" |
||||
"reflect" |
||||
"testing" |
||||
|
||||
"github.com/davecgh/go-spew/spew" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
// typeWithoutStringer is a alias for the Type type which simply doesn't implement
|
||||
// the stringer interface to allow printing type details in the tests below.
|
||||
type typeWithoutStringer Type |
||||
|
||||
// Tests that all allowed types get recognized by the type parser.
|
||||
func TestTypeRegexp(t *testing.T) { |
||||
tests := []struct { |
||||
blob string |
||||
components []ArgumentMarshaling |
||||
kind Type |
||||
}{ |
||||
{"bool", nil, Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}}, |
||||
{"bool[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]bool(nil)), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[]"}}, |
||||
{"bool[2]", nil, Type{Size: 2, Kind: reflect.Array, T: ArrayTy, Type: reflect.TypeOf([2]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[2]"}}, |
||||
{"bool[2][]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([][2]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[2]"}, stringKind: "bool[2][]"}}, |
||||
{"bool[][]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([][]bool{}), Elem: &Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[]"}, stringKind: "bool[][]"}}, |
||||
{"bool[][2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][]bool{}), Elem: &Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[]"}, stringKind: "bool[][2]"}}, |
||||
{"bool[2][2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][2]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[2]"}, stringKind: "bool[2][2]"}}, |
||||
{"bool[2][][2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][][2]bool{}), Elem: &Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([][2]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[2]"}, stringKind: "bool[2][]"}, stringKind: "bool[2][][2]"}}, |
||||
{"bool[2][2][2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][2][2]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][2]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[2]"}, stringKind: "bool[2][2]"}, stringKind: "bool[2][2][2]"}}, |
||||
{"bool[][][]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([][][]bool{}), Elem: &Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([][]bool{}), Elem: &Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[]"}, stringKind: "bool[][]"}, stringKind: "bool[][][]"}}, |
||||
{"bool[][2][]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([][2][]bool{}), Elem: &Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][]bool{}), Elem: &Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]bool{}), Elem: &Type{Kind: reflect.Bool, T: BoolTy, Type: reflect.TypeOf(bool(false)), stringKind: "bool"}, stringKind: "bool[]"}, stringKind: "bool[][2]"}, stringKind: "bool[][2][]"}}, |
||||
{"int8", nil, Type{Kind: reflect.Int8, Type: int8T, Size: 8, T: IntTy, stringKind: "int8"}}, |
||||
{"int16", nil, Type{Kind: reflect.Int16, Type: int16T, Size: 16, T: IntTy, stringKind: "int16"}}, |
||||
{"int32", nil, Type{Kind: reflect.Int32, Type: int32T, Size: 32, T: IntTy, stringKind: "int32"}}, |
||||
{"int64", nil, Type{Kind: reflect.Int64, Type: int64T, Size: 64, T: IntTy, stringKind: "int64"}}, |
||||
{"int256", nil, Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: IntTy, stringKind: "int256"}}, |
||||
{"int8[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]int8{}), Elem: &Type{Kind: reflect.Int8, Type: int8T, Size: 8, T: IntTy, stringKind: "int8"}, stringKind: "int8[]"}}, |
||||
{"int8[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]int8{}), Elem: &Type{Kind: reflect.Int8, Type: int8T, Size: 8, T: IntTy, stringKind: "int8"}, stringKind: "int8[2]"}}, |
||||
{"int16[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]int16{}), Elem: &Type{Kind: reflect.Int16, Type: int16T, Size: 16, T: IntTy, stringKind: "int16"}, stringKind: "int16[]"}}, |
||||
{"int16[2]", nil, Type{Size: 2, Kind: reflect.Array, T: ArrayTy, Type: reflect.TypeOf([2]int16{}), Elem: &Type{Kind: reflect.Int16, Type: int16T, Size: 16, T: IntTy, stringKind: "int16"}, stringKind: "int16[2]"}}, |
||||
{"int32[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]int32{}), Elem: &Type{Kind: reflect.Int32, Type: int32T, Size: 32, T: IntTy, stringKind: "int32"}, stringKind: "int32[]"}}, |
||||
{"int32[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]int32{}), Elem: &Type{Kind: reflect.Int32, Type: int32T, Size: 32, T: IntTy, stringKind: "int32"}, stringKind: "int32[2]"}}, |
||||
{"int64[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]int64{}), Elem: &Type{Kind: reflect.Int64, Type: int64T, Size: 64, T: IntTy, stringKind: "int64"}, stringKind: "int64[]"}}, |
||||
{"int64[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]int64{}), Elem: &Type{Kind: reflect.Int64, Type: int64T, Size: 64, T: IntTy, stringKind: "int64"}, stringKind: "int64[2]"}}, |
||||
{"int256[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]*big.Int{}), Elem: &Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: IntTy, stringKind: "int256"}, stringKind: "int256[]"}}, |
||||
{"int256[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]*big.Int{}), Elem: &Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: IntTy, stringKind: "int256"}, stringKind: "int256[2]"}}, |
||||
{"uint8", nil, Type{Kind: reflect.Uint8, Type: uint8T, Size: 8, T: UintTy, stringKind: "uint8"}}, |
||||
{"uint16", nil, Type{Kind: reflect.Uint16, Type: uint16T, Size: 16, T: UintTy, stringKind: "uint16"}}, |
||||
{"uint32", nil, Type{Kind: reflect.Uint32, Type: uint32T, Size: 32, T: UintTy, stringKind: "uint32"}}, |
||||
{"uint64", nil, Type{Kind: reflect.Uint64, Type: uint64T, Size: 64, T: UintTy, stringKind: "uint64"}}, |
||||
{"uint256", nil, Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: UintTy, stringKind: "uint256"}}, |
||||
{"uint8[]", nil, Type{Kind: reflect.Slice, T: SliceTy, Type: reflect.TypeOf([]uint8{}), Elem: &Type{Kind: reflect.Uint8, Type: uint8T, Size: 8, T: UintTy, stringKind: "uint8"}, stringKind: "uint8[]"}}, |
||||
{"uint8[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]uint8{}), Elem: &Type{Kind: reflect.Uint8, Type: uint8T, Size: 8, T: UintTy, stringKind: "uint8"}, stringKind: "uint8[2]"}}, |
||||
{"uint16[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]uint16{}), Elem: &Type{Kind: reflect.Uint16, Type: uint16T, Size: 16, T: UintTy, stringKind: "uint16"}, stringKind: "uint16[]"}}, |
||||
{"uint16[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]uint16{}), Elem: &Type{Kind: reflect.Uint16, Type: uint16T, Size: 16, T: UintTy, stringKind: "uint16"}, stringKind: "uint16[2]"}}, |
||||
{"uint32[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]uint32{}), Elem: &Type{Kind: reflect.Uint32, Type: uint32T, Size: 32, T: UintTy, stringKind: "uint32"}, stringKind: "uint32[]"}}, |
||||
{"uint32[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]uint32{}), Elem: &Type{Kind: reflect.Uint32, Type: uint32T, Size: 32, T: UintTy, stringKind: "uint32"}, stringKind: "uint32[2]"}}, |
||||
{"uint64[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]uint64{}), Elem: &Type{Kind: reflect.Uint64, Type: uint64T, Size: 64, T: UintTy, stringKind: "uint64"}, stringKind: "uint64[]"}}, |
||||
{"uint64[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]uint64{}), Elem: &Type{Kind: reflect.Uint64, Type: uint64T, Size: 64, T: UintTy, stringKind: "uint64"}, stringKind: "uint64[2]"}}, |
||||
{"uint256[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]*big.Int{}), Elem: &Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: UintTy, stringKind: "uint256"}, stringKind: "uint256[]"}}, |
||||
{"uint256[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Type: reflect.TypeOf([2]*big.Int{}), Size: 2, Elem: &Type{Kind: reflect.Ptr, Type: bigT, Size: 256, T: UintTy, stringKind: "uint256"}, stringKind: "uint256[2]"}}, |
||||
{"bytes32", nil, Type{Kind: reflect.Array, T: FixedBytesTy, Size: 32, Type: reflect.TypeOf([32]byte{}), stringKind: "bytes32"}}, |
||||
{"bytes[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([][]byte{}), Elem: &Type{Kind: reflect.Slice, Type: reflect.TypeOf([]byte{}), T: BytesTy, stringKind: "bytes"}, stringKind: "bytes[]"}}, |
||||
{"bytes[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][]byte{}), Elem: &Type{T: BytesTy, Type: reflect.TypeOf([]byte{}), Kind: reflect.Slice, stringKind: "bytes"}, stringKind: "bytes[2]"}}, |
||||
{"bytes32[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([][32]byte{}), Elem: &Type{Kind: reflect.Array, Type: reflect.TypeOf([32]byte{}), T: FixedBytesTy, Size: 32, stringKind: "bytes32"}, stringKind: "bytes32[]"}}, |
||||
{"bytes32[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2][32]byte{}), Elem: &Type{Kind: reflect.Array, T: FixedBytesTy, Size: 32, Type: reflect.TypeOf([32]byte{}), stringKind: "bytes32"}, stringKind: "bytes32[2]"}}, |
||||
{"string", nil, Type{Kind: reflect.String, T: StringTy, Type: reflect.TypeOf(""), stringKind: "string"}}, |
||||
{"string[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]string{}), Elem: &Type{Kind: reflect.String, Type: reflect.TypeOf(""), T: StringTy, stringKind: "string"}, stringKind: "string[]"}}, |
||||
{"string[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]string{}), Elem: &Type{Kind: reflect.String, T: StringTy, Type: reflect.TypeOf(""), stringKind: "string"}, stringKind: "string[2]"}}, |
||||
{"address", nil, Type{Kind: reflect.Array, Type: addressT, Size: 20, T: AddressTy, stringKind: "address"}}, |
||||
{"address[]", nil, Type{T: SliceTy, Kind: reflect.Slice, Type: reflect.TypeOf([]common.Address{}), Elem: &Type{Kind: reflect.Array, Type: addressT, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[]"}}, |
||||
{"address[2]", nil, Type{Kind: reflect.Array, T: ArrayTy, Size: 2, Type: reflect.TypeOf([2]common.Address{}), Elem: &Type{Kind: reflect.Array, Type: addressT, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[2]"}}, |
||||
// TODO when fixed types are implemented properly
|
||||
// {"fixed", nil, Type{}},
|
||||
// {"fixed128x128", nil, Type{}},
|
||||
// {"fixed[]", nil, Type{}},
|
||||
// {"fixed[2]", nil, Type{}},
|
||||
// {"fixed128x128[]", nil, Type{}},
|
||||
// {"fixed128x128[2]", nil, Type{}},
|
||||
{"tuple", []ArgumentMarshaling{{Name: "a", Type: "int64"}}, Type{Kind: reflect.Struct, T: TupleTy, Type: reflect.TypeOf(struct { |
||||
A int64 `json:"a"` |
||||
}{}), stringKind: "(int64)", |
||||
TupleElems: []*Type{{Kind: reflect.Int64, T: IntTy, Type: reflect.TypeOf(int64(0)), Size: 64, stringKind: "int64"}}, TupleRawNames: []string{"a"}}}, |
||||
{"tuple with long name", []ArgumentMarshaling{{Name: "aTypicalParamName", Type: "int64"}}, Type{Kind: reflect.Struct, T: TupleTy, Type: reflect.TypeOf(struct { |
||||
ATypicalParamName int64 `json:"aTypicalParamName"` |
||||
}{}), stringKind: "(int64)", |
||||
TupleElems: []*Type{{Kind: reflect.Int64, T: IntTy, Type: reflect.TypeOf(int64(0)), Size: 64, stringKind: "int64"}}, TupleRawNames: []string{"aTypicalParamName"}}}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
typ, err := NewType(tt.blob, tt.components) |
||||
if err != nil { |
||||
t.Errorf("type %q: failed to parse type string: %v", tt.blob, err) |
||||
} |
||||
if !reflect.DeepEqual(typ, tt.kind) { |
||||
t.Errorf("type %q: parsed type mismatch:\nGOT %s\nWANT %s ", tt.blob, spew.Sdump(typeWithoutStringer(typ)), spew.Sdump(typeWithoutStringer(tt.kind))) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestTypeCheck(t *testing.T) { |
||||
for i, test := range []struct { |
||||
typ string |
||||
components []ArgumentMarshaling |
||||
input interface{} |
||||
err string |
||||
}{ |
||||
{"uint", nil, big.NewInt(1), "unsupported arg type: uint"}, |
||||
{"int", nil, big.NewInt(1), "unsupported arg type: int"}, |
||||
{"uint256", nil, big.NewInt(1), ""}, |
||||
{"uint256[][3][]", nil, [][3][]*big.Int{{{}}}, ""}, |
||||
{"uint256[][][3]", nil, [3][][]*big.Int{{{}}}, ""}, |
||||
{"uint256[3][][]", nil, [][][3]*big.Int{{{}}}, ""}, |
||||
{"uint256[3][3][3]", nil, [3][3][3]*big.Int{{{}}}, ""}, |
||||
{"uint8[][]", nil, [][]uint8{}, ""}, |
||||
{"int256", nil, big.NewInt(1), ""}, |
||||
{"uint8", nil, uint8(1), ""}, |
||||
{"uint16", nil, uint16(1), ""}, |
||||
{"uint32", nil, uint32(1), ""}, |
||||
{"uint64", nil, uint64(1), ""}, |
||||
{"int8", nil, int8(1), ""}, |
||||
{"int16", nil, int16(1), ""}, |
||||
{"int32", nil, int32(1), ""}, |
||||
{"int64", nil, int64(1), ""}, |
||||
{"uint24", nil, big.NewInt(1), ""}, |
||||
{"uint40", nil, big.NewInt(1), ""}, |
||||
{"uint48", nil, big.NewInt(1), ""}, |
||||
{"uint56", nil, big.NewInt(1), ""}, |
||||
{"uint72", nil, big.NewInt(1), ""}, |
||||
{"uint80", nil, big.NewInt(1), ""}, |
||||
{"uint88", nil, big.NewInt(1), ""}, |
||||
{"uint96", nil, big.NewInt(1), ""}, |
||||
{"uint104", nil, big.NewInt(1), ""}, |
||||
{"uint112", nil, big.NewInt(1), ""}, |
||||
{"uint120", nil, big.NewInt(1), ""}, |
||||
{"uint128", nil, big.NewInt(1), ""}, |
||||
{"uint136", nil, big.NewInt(1), ""}, |
||||
{"uint144", nil, big.NewInt(1), ""}, |
||||
{"uint152", nil, big.NewInt(1), ""}, |
||||
{"uint160", nil, big.NewInt(1), ""}, |
||||
{"uint168", nil, big.NewInt(1), ""}, |
||||
{"uint176", nil, big.NewInt(1), ""}, |
||||
{"uint184", nil, big.NewInt(1), ""}, |
||||
{"uint192", nil, big.NewInt(1), ""}, |
||||
{"uint200", nil, big.NewInt(1), ""}, |
||||
{"uint208", nil, big.NewInt(1), ""}, |
||||
{"uint216", nil, big.NewInt(1), ""}, |
||||
{"uint224", nil, big.NewInt(1), ""}, |
||||
{"uint232", nil, big.NewInt(1), ""}, |
||||
{"uint240", nil, big.NewInt(1), ""}, |
||||
{"uint248", nil, big.NewInt(1), ""}, |
||||
{"int24", nil, big.NewInt(1), ""}, |
||||
{"int40", nil, big.NewInt(1), ""}, |
||||
{"int48", nil, big.NewInt(1), ""}, |
||||
{"int56", nil, big.NewInt(1), ""}, |
||||
{"int72", nil, big.NewInt(1), ""}, |
||||
{"int80", nil, big.NewInt(1), ""}, |
||||
{"int88", nil, big.NewInt(1), ""}, |
||||
{"int96", nil, big.NewInt(1), ""}, |
||||
{"int104", nil, big.NewInt(1), ""}, |
||||
{"int112", nil, big.NewInt(1), ""}, |
||||
{"int120", nil, big.NewInt(1), ""}, |
||||
{"int128", nil, big.NewInt(1), ""}, |
||||
{"int136", nil, big.NewInt(1), ""}, |
||||
{"int144", nil, big.NewInt(1), ""}, |
||||
{"int152", nil, big.NewInt(1), ""}, |
||||
{"int160", nil, big.NewInt(1), ""}, |
||||
{"int168", nil, big.NewInt(1), ""}, |
||||
{"int176", nil, big.NewInt(1), ""}, |
||||
{"int184", nil, big.NewInt(1), ""}, |
||||
{"int192", nil, big.NewInt(1), ""}, |
||||
{"int200", nil, big.NewInt(1), ""}, |
||||
{"int208", nil, big.NewInt(1), ""}, |
||||
{"int216", nil, big.NewInt(1), ""}, |
||||
{"int224", nil, big.NewInt(1), ""}, |
||||
{"int232", nil, big.NewInt(1), ""}, |
||||
{"int240", nil, big.NewInt(1), ""}, |
||||
{"int248", nil, big.NewInt(1), ""}, |
||||
{"uint30", nil, uint8(1), "abi: cannot use uint8 as type ptr as argument"}, |
||||
{"uint8", nil, uint16(1), "abi: cannot use uint16 as type uint8 as argument"}, |
||||
{"uint8", nil, uint32(1), "abi: cannot use uint32 as type uint8 as argument"}, |
||||
{"uint8", nil, uint64(1), "abi: cannot use uint64 as type uint8 as argument"}, |
||||
{"uint8", nil, int8(1), "abi: cannot use int8 as type uint8 as argument"}, |
||||
{"uint8", nil, int16(1), "abi: cannot use int16 as type uint8 as argument"}, |
||||
{"uint8", nil, int32(1), "abi: cannot use int32 as type uint8 as argument"}, |
||||
{"uint8", nil, int64(1), "abi: cannot use int64 as type uint8 as argument"}, |
||||
{"uint16", nil, uint16(1), ""}, |
||||
{"uint16", nil, uint8(1), "abi: cannot use uint8 as type uint16 as argument"}, |
||||
{"uint16[]", nil, []uint16{1, 2, 3}, ""}, |
||||
{"uint16[]", nil, [3]uint16{1, 2, 3}, ""}, |
||||
{"uint16[]", nil, []uint32{1, 2, 3}, "abi: cannot use []uint32 as type [0]uint16 as argument"}, |
||||
{"uint16[3]", nil, [3]uint32{1, 2, 3}, "abi: cannot use [3]uint32 as type [3]uint16 as argument"}, |
||||
{"uint16[3]", nil, [4]uint16{1, 2, 3}, "abi: cannot use [4]uint16 as type [3]uint16 as argument"}, |
||||
{"uint16[3]", nil, []uint16{1, 2, 3}, ""}, |
||||
{"uint16[3]", nil, []uint16{1, 2, 3, 4}, "abi: cannot use [4]uint16 as type [3]uint16 as argument"}, |
||||
{"address[]", nil, []common.Address{{1}}, ""}, |
||||
{"address[1]", nil, []common.Address{{1}}, ""}, |
||||
{"address[1]", nil, [1]common.Address{{1}}, ""}, |
||||
{"address[2]", nil, [1]common.Address{{1}}, "abi: cannot use [1]array as type [2]array as argument"}, |
||||
{"bytes32", nil, [32]byte{}, ""}, |
||||
{"bytes31", nil, [31]byte{}, ""}, |
||||
{"bytes30", nil, [30]byte{}, ""}, |
||||
{"bytes29", nil, [29]byte{}, ""}, |
||||
{"bytes28", nil, [28]byte{}, ""}, |
||||
{"bytes27", nil, [27]byte{}, ""}, |
||||
{"bytes26", nil, [26]byte{}, ""}, |
||||
{"bytes25", nil, [25]byte{}, ""}, |
||||
{"bytes24", nil, [24]byte{}, ""}, |
||||
{"bytes23", nil, [23]byte{}, ""}, |
||||
{"bytes22", nil, [22]byte{}, ""}, |
||||
{"bytes21", nil, [21]byte{}, ""}, |
||||
{"bytes20", nil, [20]byte{}, ""}, |
||||
{"bytes19", nil, [19]byte{}, ""}, |
||||
{"bytes18", nil, [18]byte{}, ""}, |
||||
{"bytes17", nil, [17]byte{}, ""}, |
||||
{"bytes16", nil, [16]byte{}, ""}, |
||||
{"bytes15", nil, [15]byte{}, ""}, |
||||
{"bytes14", nil, [14]byte{}, ""}, |
||||
{"bytes13", nil, [13]byte{}, ""}, |
||||
{"bytes12", nil, [12]byte{}, ""}, |
||||
{"bytes11", nil, [11]byte{}, ""}, |
||||
{"bytes10", nil, [10]byte{}, ""}, |
||||
{"bytes9", nil, [9]byte{}, ""}, |
||||
{"bytes8", nil, [8]byte{}, ""}, |
||||
{"bytes7", nil, [7]byte{}, ""}, |
||||
{"bytes6", nil, [6]byte{}, ""}, |
||||
{"bytes5", nil, [5]byte{}, ""}, |
||||
{"bytes4", nil, [4]byte{}, ""}, |
||||
{"bytes3", nil, [3]byte{}, ""}, |
||||
{"bytes2", nil, [2]byte{}, ""}, |
||||
{"bytes1", nil, [1]byte{}, ""}, |
||||
{"bytes32", nil, [33]byte{}, "abi: cannot use [33]uint8 as type [32]uint8 as argument"}, |
||||
{"bytes32", nil, common.Hash{1}, ""}, |
||||
{"bytes31", nil, common.Hash{1}, "abi: cannot use common.Hash as type [31]uint8 as argument"}, |
||||
{"bytes31", nil, [32]byte{}, "abi: cannot use [32]uint8 as type [31]uint8 as argument"}, |
||||
{"bytes", nil, []byte{0, 1}, ""}, |
||||
{"bytes", nil, [2]byte{0, 1}, "abi: cannot use array as type slice as argument"}, |
||||
{"bytes", nil, common.Hash{1}, "abi: cannot use array as type slice as argument"}, |
||||
{"string", nil, "hello world", ""}, |
||||
{"string", nil, string(""), ""}, |
||||
{"string", nil, []byte{}, "abi: cannot use slice as type string as argument"}, |
||||
{"bytes32[]", nil, [][32]byte{{}}, ""}, |
||||
{"function", nil, [24]byte{}, ""}, |
||||
{"bytes20", nil, common.Address{}, ""}, |
||||
{"address", nil, [20]byte{}, ""}, |
||||
{"address", nil, common.Address{}, ""}, |
||||
{"bytes32[]]", nil, "", "invalid arg type in abi"}, |
||||
{"invalidType", nil, "", "unsupported arg type: invalidType"}, |
||||
{"invalidSlice[]", nil, "", "unsupported arg type: invalidSlice"}, |
||||
// simple tuple
|
||||
{"tuple", []ArgumentMarshaling{{Name: "a", Type: "uint256"}, {Name: "b", Type: "uint256"}}, struct { |
||||
A *big.Int |
||||
B *big.Int |
||||
}{}, ""}, |
||||
// tuple slice
|
||||
{"tuple[]", []ArgumentMarshaling{{Name: "a", Type: "uint256"}, {Name: "b", Type: "uint256"}}, []struct { |
||||
A *big.Int |
||||
B *big.Int |
||||
}{}, ""}, |
||||
// tuple array
|
||||
{"tuple[2]", []ArgumentMarshaling{{Name: "a", Type: "uint256"}, {Name: "b", Type: "uint256"}}, []struct { |
||||
A *big.Int |
||||
B *big.Int |
||||
}{{big.NewInt(0), big.NewInt(0)}, {big.NewInt(0), big.NewInt(0)}}, ""}, |
||||
} { |
||||
typ, err := NewType(test.typ, test.components) |
||||
if err != nil && len(test.err) == 0 { |
||||
t.Fatal("unexpected parse error:", err) |
||||
} else if err != nil && len(test.err) != 0 { |
||||
if err.Error() != test.err { |
||||
t.Errorf("%d failed. Expected err: '%v' got err: '%v'", i, test.err, err) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
err = typeCheck(typ, reflect.ValueOf(test.input)) |
||||
if err != nil && len(test.err) == 0 { |
||||
t.Errorf("%d failed. Expected no err but got: %v", i, err) |
||||
continue |
||||
} |
||||
if err == nil && len(test.err) != 0 { |
||||
t.Errorf("%d failed. Expected err: %v but got none", i, test.err) |
||||
continue |
||||
} |
||||
|
||||
if err != nil && len(test.err) != 0 && err.Error() != test.err { |
||||
t.Errorf("%d failed. Expected err: '%v' got err: '%v'", i, test.err, err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,295 @@ |
||||
// 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 abi |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"fmt" |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
var ( |
||||
maxUint256 = big.NewInt(0).Add( |
||||
big.NewInt(0).Exp(big.NewInt(2), big.NewInt(256), nil), |
||||
big.NewInt(-1)) |
||||
maxInt256 = big.NewInt(0).Add( |
||||
big.NewInt(0).Exp(big.NewInt(2), big.NewInt(255), nil), |
||||
big.NewInt(-1)) |
||||
) |
||||
|
||||
// reads the integer based on its kind
|
||||
func readInteger(typ byte, kind reflect.Kind, b []byte) interface{} { |
||||
switch kind { |
||||
case reflect.Uint8: |
||||
return b[len(b)-1] |
||||
case reflect.Uint16: |
||||
return binary.BigEndian.Uint16(b[len(b)-2:]) |
||||
case reflect.Uint32: |
||||
return binary.BigEndian.Uint32(b[len(b)-4:]) |
||||
case reflect.Uint64: |
||||
return binary.BigEndian.Uint64(b[len(b)-8:]) |
||||
case reflect.Int8: |
||||
return int8(b[len(b)-1]) |
||||
case reflect.Int16: |
||||
return int16(binary.BigEndian.Uint16(b[len(b)-2:])) |
||||
case reflect.Int32: |
||||
return int32(binary.BigEndian.Uint32(b[len(b)-4:])) |
||||
case reflect.Int64: |
||||
return int64(binary.BigEndian.Uint64(b[len(b)-8:])) |
||||
default: |
||||
// the only case lefts for integer is int256/uint256.
|
||||
// big.SetBytes can't tell if a number is negative, positive on itself.
|
||||
// On EVM, if the returned number > max int256, it is negative.
|
||||
ret := new(big.Int).SetBytes(b) |
||||
if typ == UintTy { |
||||
return ret |
||||
} |
||||
|
||||
if ret.Cmp(maxInt256) > 0 { |
||||
ret.Add(maxUint256, big.NewInt(0).Neg(ret)) |
||||
ret.Add(ret, big.NewInt(1)) |
||||
ret.Neg(ret) |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
|
||||
// reads a bool
|
||||
func readBool(word []byte) (bool, error) { |
||||
for _, b := range word[:31] { |
||||
if b != 0 { |
||||
return false, errBadBool |
||||
} |
||||
} |
||||
switch word[31] { |
||||
case 0: |
||||
return false, nil |
||||
case 1: |
||||
return true, nil |
||||
default: |
||||
return false, errBadBool |
||||
} |
||||
} |
||||
|
||||
// A function type is simply the address with the function selection signature at the end.
|
||||
// This enforces that standard by always presenting it as a 24-array (address + sig = 24 bytes)
|
||||
func readFunctionType(t Type, word []byte) (funcTy [24]byte, err error) { |
||||
if t.T != FunctionTy { |
||||
return [24]byte{}, fmt.Errorf("abi: invalid type in call to make function type byte array") |
||||
} |
||||
if garbage := binary.BigEndian.Uint64(word[24:32]); garbage != 0 { |
||||
err = fmt.Errorf("abi: got improperly encoded function type, got %v", word) |
||||
} else { |
||||
copy(funcTy[:], word[0:24]) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// through reflection, creates a fixed array to be read from
|
||||
func readFixedBytes(t Type, word []byte) (interface{}, error) { |
||||
if t.T != FixedBytesTy { |
||||
return nil, fmt.Errorf("abi: invalid type in call to make fixed byte array") |
||||
} |
||||
// convert
|
||||
array := reflect.New(t.Type).Elem() |
||||
|
||||
reflect.Copy(array, reflect.ValueOf(word[0:t.Size])) |
||||
return array.Interface(), nil |
||||
|
||||
} |
||||
|
||||
// iteratively unpack elements
|
||||
func forEachUnpack(t Type, output []byte, start, size int) (interface{}, error) { |
||||
if size < 0 { |
||||
return nil, fmt.Errorf("cannot marshal input to array, size is negative (%d)", size) |
||||
} |
||||
if start+32*size > len(output) { |
||||
return nil, fmt.Errorf("abi: cannot marshal in to go array: offset %d would go over slice boundary (len=%d)", len(output), start+32*size) |
||||
} |
||||
|
||||
// this value will become our slice or our array, depending on the type
|
||||
var refSlice reflect.Value |
||||
|
||||
if t.T == SliceTy { |
||||
// declare our slice
|
||||
refSlice = reflect.MakeSlice(t.Type, size, size) |
||||
} else if t.T == ArrayTy { |
||||
// declare our array
|
||||
refSlice = reflect.New(t.Type).Elem() |
||||
} else { |
||||
return nil, fmt.Errorf("abi: invalid type in array/slice unpacking stage") |
||||
} |
||||
|
||||
// Arrays have packed elements, resulting in longer unpack steps.
|
||||
// Slices have just 32 bytes per element (pointing to the contents).
|
||||
elemSize := getTypeSize(*t.Elem) |
||||
|
||||
for i, j := start, 0; j < size; i, j = i+elemSize, j+1 { |
||||
inter, err := toGoType(i, *t.Elem, output) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// append the item to our reflect slice
|
||||
refSlice.Index(j).Set(reflect.ValueOf(inter)) |
||||
} |
||||
|
||||
// return the interface
|
||||
return refSlice.Interface(), nil |
||||
} |
||||
|
||||
func forTupleUnpack(t Type, output []byte) (interface{}, error) { |
||||
retval := reflect.New(t.Type).Elem() |
||||
virtualArgs := 0 |
||||
for index, elem := range t.TupleElems { |
||||
marshalledValue, err := toGoType((index+virtualArgs)*32, *elem, output) |
||||
if elem.T == ArrayTy && !isDynamicType(*elem) { |
||||
// If we have a static array, like [3]uint256, these are coded as
|
||||
// just like uint256,uint256,uint256.
|
||||
// This means that we need to add two 'virtual' arguments when
|
||||
// we count the index from now on.
|
||||
//
|
||||
// Array values nested multiple levels deep are also encoded inline:
|
||||
// [2][3]uint256: uint256,uint256,uint256,uint256,uint256,uint256
|
||||
//
|
||||
// Calculate the full array size to get the correct offset for the next argument.
|
||||
// Decrement it by 1, as the normal index increment is still applied.
|
||||
virtualArgs += getTypeSize(*elem)/32 - 1 |
||||
} else if elem.T == TupleTy && !isDynamicType(*elem) { |
||||
// If we have a static tuple, like (uint256, bool, uint256), these are
|
||||
// coded as just like uint256,bool,uint256
|
||||
virtualArgs += getTypeSize(*elem)/32 - 1 |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
retval.Field(index).Set(reflect.ValueOf(marshalledValue)) |
||||
} |
||||
return retval.Interface(), nil |
||||
} |
||||
|
||||
// toGoType parses the output bytes and recursively assigns the value of these bytes
|
||||
// into a go type with accordance with the ABI spec.
|
||||
func toGoType(index int, t Type, output []byte) (interface{}, error) { |
||||
if index+32 > len(output) { |
||||
return nil, fmt.Errorf("abi: cannot marshal in to go type: length insufficient %d require %d", len(output), index+32) |
||||
} |
||||
|
||||
var ( |
||||
returnOutput []byte |
||||
begin, length int |
||||
err error |
||||
) |
||||
|
||||
// if we require a length prefix, find the beginning word and size returned.
|
||||
if t.requiresLengthPrefix() { |
||||
begin, length, err = lengthPrefixPointsTo(index, output) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
returnOutput = output[index : index+32] |
||||
} |
||||
|
||||
switch t.T { |
||||
case TupleTy: |
||||
if isDynamicType(t) { |
||||
begin, err := tuplePointsTo(index, output) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return forTupleUnpack(t, output[begin:]) |
||||
} else { |
||||
return forTupleUnpack(t, output[index:]) |
||||
} |
||||
case SliceTy: |
||||
return forEachUnpack(t, output[begin:], 0, length) |
||||
case ArrayTy: |
||||
if isDynamicType(*t.Elem) { |
||||
offset := int64(binary.BigEndian.Uint64(returnOutput[len(returnOutput)-8:])) |
||||
return forEachUnpack(t, output[offset:], 0, t.Size) |
||||
} |
||||
return forEachUnpack(t, output[index:], 0, t.Size) |
||||
case StringTy: // variable arrays are written at the end of the return bytes
|
||||
return string(output[begin : begin+length]), nil |
||||
case IntTy, UintTy: |
||||
return readInteger(t.T, t.Kind, returnOutput), nil |
||||
case BoolTy: |
||||
return readBool(returnOutput) |
||||
case AddressTy: |
||||
return common.BytesToAddress(returnOutput), nil |
||||
case HashTy: |
||||
return common.BytesToHash(returnOutput), nil |
||||
case BytesTy: |
||||
return output[begin : begin+length], nil |
||||
case FixedBytesTy: |
||||
return readFixedBytes(t, returnOutput) |
||||
case FunctionTy: |
||||
return readFunctionType(t, returnOutput) |
||||
default: |
||||
return nil, fmt.Errorf("abi: unknown type %v", t.T) |
||||
} |
||||
} |
||||
|
||||
// interprets a 32 byte slice as an offset and then determines which indice to look to decode the type.
|
||||
func lengthPrefixPointsTo(index int, output []byte) (start int, length int, err error) { |
||||
bigOffsetEnd := big.NewInt(0).SetBytes(output[index : index+32]) |
||||
bigOffsetEnd.Add(bigOffsetEnd, common.Big32) |
||||
outputLength := big.NewInt(int64(len(output))) |
||||
|
||||
if bigOffsetEnd.Cmp(outputLength) > 0 { |
||||
return 0, 0, fmt.Errorf("abi: cannot marshal in to go slice: offset %v would go over slice boundary (len=%v)", bigOffsetEnd, outputLength) |
||||
} |
||||
|
||||
if bigOffsetEnd.BitLen() > 63 { |
||||
return 0, 0, fmt.Errorf("abi offset larger than int64: %v", bigOffsetEnd) |
||||
} |
||||
|
||||
offsetEnd := int(bigOffsetEnd.Uint64()) |
||||
lengthBig := big.NewInt(0).SetBytes(output[offsetEnd-32 : offsetEnd]) |
||||
|
||||
totalSize := big.NewInt(0) |
||||
totalSize.Add(totalSize, bigOffsetEnd) |
||||
totalSize.Add(totalSize, lengthBig) |
||||
if totalSize.BitLen() > 63 { |
||||
return 0, 0, fmt.Errorf("abi: length larger than int64: %v", totalSize) |
||||
} |
||||
|
||||
if totalSize.Cmp(outputLength) > 0 { |
||||
return 0, 0, fmt.Errorf("abi: cannot marshal in to go type: length insufficient %v require %v", outputLength, totalSize) |
||||
} |
||||
start = int(bigOffsetEnd.Uint64()) |
||||
length = int(lengthBig.Uint64()) |
||||
return |
||||
} |
||||
|
||||
// tuplePointsTo resolves the location reference for dynamic tuple.
|
||||
func tuplePointsTo(index int, output []byte) (start int, err error) { |
||||
offset := big.NewInt(0).SetBytes(output[index : index+32]) |
||||
outputLen := big.NewInt(int64(len(output))) |
||||
|
||||
if offset.Cmp(big.NewInt(int64(len(output)))) > 0 { |
||||
return 0, fmt.Errorf("abi: cannot marshal in to go slice: offset %v would go over slice boundary (len=%v)", offset, outputLen) |
||||
} |
||||
if offset.BitLen() > 63 { |
||||
return 0, fmt.Errorf("abi offset larger than int64: %v", offset) |
||||
} |
||||
return int(offset.Uint64()), nil |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,222 @@ |
||||
// 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 accounts implements high level Ethereum account management.
|
||||
package accounts |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/big" |
||||
|
||||
ethereum "github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
"golang.org/x/crypto/sha3" |
||||
) |
||||
|
||||
// Account represents an Ethereum account located at a specific location defined
|
||||
// by the optional URL field.
|
||||
type Account struct { |
||||
Address common.Address `json:"address"` // Ethereum account address derived from the key
|
||||
URL URL `json:"url"` // Optional resource locator within a backend
|
||||
} |
||||
|
||||
const ( |
||||
MimetypeTextWithValidator = "text/validator" |
||||
MimetypeTypedData = "data/typed" |
||||
MimetypeClique = "application/x-clique-header" |
||||
MimetypeTextPlain = "text/plain" |
||||
) |
||||
|
||||
// Wallet represents a software or hardware wallet that might contain one or more
|
||||
// accounts (derived from the same seed).
|
||||
type Wallet interface { |
||||
// URL retrieves the canonical path under which this wallet is reachable. It is
|
||||
// user by upper layers to define a sorting order over all wallets from multiple
|
||||
// backends.
|
||||
URL() URL |
||||
|
||||
// Status returns a textual status to aid the user in the current state of the
|
||||
// wallet. It also returns an error indicating any failure the wallet might have
|
||||
// encountered.
|
||||
Status() (string, error) |
||||
|
||||
// Open initializes access to a wallet instance. It is not meant to unlock or
|
||||
// decrypt account keys, rather simply to establish a connection to hardware
|
||||
// wallets and/or to access derivation seeds.
|
||||
//
|
||||
// The passphrase parameter may or may not be used by the implementation of a
|
||||
// particular wallet instance. The reason there is no passwordless open method
|
||||
// is to strive towards a uniform wallet handling, oblivious to the different
|
||||
// backend providers.
|
||||
//
|
||||
// Please note, if you open a wallet, you must close it to release any allocated
|
||||
// resources (especially important when working with hardware wallets).
|
||||
Open(passphrase string) error |
||||
|
||||
// Close releases any resources held by an open wallet instance.
|
||||
Close() error |
||||
|
||||
// Accounts retrieves the list of signing accounts the wallet is currently aware
|
||||
// of. For hierarchical deterministic wallets, the list will not be exhaustive,
|
||||
// rather only contain the accounts explicitly pinned during account derivation.
|
||||
Accounts() []Account |
||||
|
||||
// Contains returns whether an account is part of this particular wallet or not.
|
||||
Contains(account Account) bool |
||||
|
||||
// Derive attempts to explicitly derive a hierarchical deterministic account at
|
||||
// the specified derivation path. If requested, the derived account will be added
|
||||
// to the wallet's tracked account list.
|
||||
Derive(path DerivationPath, pin bool) (Account, error) |
||||
|
||||
// SelfDerive sets a base account derivation path from which the wallet attempts
|
||||
// to discover non zero accounts and automatically add them to list of tracked
|
||||
// accounts.
|
||||
//
|
||||
// Note, self derivaton will increment the last component of the specified path
|
||||
// opposed to decending into a child path to allow discovering accounts starting
|
||||
// from non zero components.
|
||||
//
|
||||
// Some hardware wallets switched derivation paths through their evolution, so
|
||||
// this method supports providing multiple bases to discover old user accounts
|
||||
// too. Only the last base will be used to derive the next empty account.
|
||||
//
|
||||
// You can disable automatic account discovery by calling SelfDerive with a nil
|
||||
// chain state reader.
|
||||
SelfDerive(bases []DerivationPath, chain ethereum.ChainStateReader) |
||||
|
||||
// SignData requests the wallet to sign the hash of the given data
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
//
|
||||
// If the wallet requires additional authentication to sign the request (e.g.
|
||||
// a password to decrypt the account, or a PIN code o verify the transaction),
|
||||
// an AuthNeededError instance will be returned, containing infos for the user
|
||||
// about which fields or actions are needed. The user may retry by providing
|
||||
// the needed details via SignDataWithPassphrase, or by other means (e.g. unlock
|
||||
// the account in a keystore).
|
||||
SignData(account Account, mimeType string, data []byte) ([]byte, error) |
||||
|
||||
// SignDataWithPassphrase is identical to SignData, but also takes a password
|
||||
// NOTE: there's an chance that an erroneous call might mistake the two strings, and
|
||||
// supply password in the mimetype field, or vice versa. Thus, an implementation
|
||||
// should never echo the mimetype or return the mimetype in the error-response
|
||||
SignDataWithPassphrase(account Account, passphrase, mimeType string, data []byte) ([]byte, error) |
||||
|
||||
// SignText requests the wallet to sign the hash of a given piece of data, prefixed
|
||||
// by the Ethereum prefix scheme
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
//
|
||||
// If the wallet requires additional authentication to sign the request (e.g.
|
||||
// a password to decrypt the account, or a PIN code o verify the transaction),
|
||||
// an AuthNeededError instance will be returned, containing infos for the user
|
||||
// about which fields or actions are needed. The user may retry by providing
|
||||
// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
|
||||
// the account in a keystore).
|
||||
SignText(account Account, text []byte) ([]byte, error) |
||||
|
||||
// SignTextWithPassphrase is identical to Signtext, but also takes a password
|
||||
SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) |
||||
|
||||
// SignTx requests the wallet to sign the given transaction.
|
||||
//
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
//
|
||||
// If the wallet requires additional authentication to sign the request (e.g.
|
||||
// a password to decrypt the account, or a PIN code to verify the transaction),
|
||||
// an AuthNeededError instance will be returned, containing infos for the user
|
||||
// about which fields or actions are needed. The user may retry by providing
|
||||
// the needed details via SignTxWithPassphrase, or by other means (e.g. unlock
|
||||
// the account in a keystore).
|
||||
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) |
||||
|
||||
// SignTxWithPassphrase is identical to SignTx, but also takes a password
|
||||
SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) |
||||
} |
||||
|
||||
// Backend is a "wallet provider" that may contain a batch of accounts they can
|
||||
// sign transactions with and upon request, do so.
|
||||
type Backend interface { |
||||
// Wallets retrieves the list of wallets the backend is currently aware of.
|
||||
//
|
||||
// The returned wallets are not opened by default. For software HD wallets this
|
||||
// means that no base seeds are decrypted, and for hardware wallets that no actual
|
||||
// connection is established.
|
||||
//
|
||||
// The resulting wallet list will be sorted alphabetically based on its internal
|
||||
// URL assigned by the backend. Since wallets (especially hardware) may come and
|
||||
// go, the same wallet might appear at a different positions in the list during
|
||||
// subsequent retrievals.
|
||||
Wallets() []Wallet |
||||
|
||||
// Subscribe creates an async subscription to receive notifications when the
|
||||
// backend detects the arrival or departure of a wallet.
|
||||
Subscribe(sink chan<- WalletEvent) event.Subscription |
||||
} |
||||
|
||||
// TextHash is a helper function that calculates a hash for the given message that can be
|
||||
// safely used to calculate a signature from.
|
||||
//
|
||||
// The hash is calulcated as
|
||||
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
|
||||
//
|
||||
// This gives context to the signed message and prevents signing of transactions.
|
||||
func TextHash(data []byte) []byte { |
||||
hash, _ := TextAndHash(data) |
||||
return hash |
||||
} |
||||
|
||||
// TextAndHash is a helper function that calculates a hash for the given message that can be
|
||||
// safely used to calculate a signature from.
|
||||
//
|
||||
// The hash is calulcated as
|
||||
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
|
||||
//
|
||||
// This gives context to the signed message and prevents signing of transactions.
|
||||
func TextAndHash(data []byte) ([]byte, string) { |
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), string(data)) |
||||
hasher := sha3.NewLegacyKeccak256() |
||||
hasher.Write([]byte(msg)) |
||||
return hasher.Sum(nil), msg |
||||
} |
||||
|
||||
// WalletEventType represents the different event types that can be fired by
|
||||
// the wallet subscription subsystem.
|
||||
type WalletEventType int |
||||
|
||||
const ( |
||||
// WalletArrived is fired when a new wallet is detected either via USB or via
|
||||
// a filesystem event in the keystore.
|
||||
WalletArrived WalletEventType = iota |
||||
|
||||
// WalletOpened is fired when a wallet is successfully opened with the purpose
|
||||
// of starting any background processes such as automatic key derivation.
|
||||
WalletOpened |
||||
|
||||
// WalletDropped
|
||||
WalletDropped |
||||
) |
||||
|
||||
// WalletEvent is an event fired by an account backend when a wallet arrival or
|
||||
// departure is detected.
|
||||
type WalletEvent struct { |
||||
Wallet Wallet // Wallet instance arrived or departed
|
||||
Kind WalletEventType // Event type that happened in the system
|
||||
} |
@ -0,0 +1,32 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
) |
||||
|
||||
func TestTextHash(t *testing.T) { |
||||
hash := TextHash([]byte("Hello Joe")) |
||||
want := hexutil.MustDecode("0xa080337ae51c4e064c189e113edd0ba391df9206e2f49db658bb32cf2911730b") |
||||
if !bytes.Equal(hash, want) { |
||||
t.Fatalf("wrong hash: %x", hash) |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
) |
||||
|
||||
// ErrUnknownAccount is returned for any requested operation for which no backend
|
||||
// provides the specified account.
|
||||
var ErrUnknownAccount = errors.New("unknown account") |
||||
|
||||
// ErrUnknownWallet is returned for any requested operation for which no backend
|
||||
// provides the specified wallet.
|
||||
var ErrUnknownWallet = errors.New("unknown wallet") |
||||
|
||||
// ErrNotSupported is returned when an operation is requested from an account
|
||||
// backend that it does not support.
|
||||
var ErrNotSupported = errors.New("not supported") |
||||
|
||||
// ErrInvalidPassphrase is returned when a decryption operation receives a bad
|
||||
// passphrase.
|
||||
var ErrInvalidPassphrase = errors.New("invalid passphrase") |
||||
|
||||
// ErrWalletAlreadyOpen is returned if a wallet is attempted to be opened the
|
||||
// second time.
|
||||
var ErrWalletAlreadyOpen = errors.New("wallet already open") |
||||
|
||||
// ErrWalletClosed is returned if a wallet is attempted to be opened the
|
||||
// secodn time.
|
||||
var ErrWalletClosed = errors.New("wallet closed") |
||||
|
||||
// AuthNeededError is returned by backends for signing requests where the user
|
||||
// is required to provide further authentication before signing can succeed.
|
||||
//
|
||||
// This usually means either that a password needs to be supplied, or perhaps a
|
||||
// one time PIN code displayed by some hardware device.
|
||||
type AuthNeededError struct { |
||||
Needed string // Extra authentication the user needs to provide
|
||||
} |
||||
|
||||
// NewAuthNeededError creates a new authentication error with the extra details
|
||||
// about the needed fields set.
|
||||
func NewAuthNeededError(needed string) error { |
||||
return &AuthNeededError{ |
||||
Needed: needed, |
||||
} |
||||
} |
||||
|
||||
// Error implements the standard error interface.
|
||||
func (err *AuthNeededError) Error() string { |
||||
return fmt.Sprintf("authentication needed: %s", err.Needed) |
||||
} |
@ -0,0 +1,228 @@ |
||||
// 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 external |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/big" |
||||
"sync" |
||||
|
||||
"github.com/ethereum/go-ethereum" |
||||
"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/event" |
||||
"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" |
||||
) |
||||
|
||||
type ExternalBackend struct { |
||||
signers []accounts.Wallet |
||||
} |
||||
|
||||
func (eb *ExternalBackend) Wallets() []accounts.Wallet { |
||||
return eb.signers |
||||
} |
||||
|
||||
func NewExternalBackend(endpoint string) (*ExternalBackend, error) { |
||||
signer, err := NewExternalSigner(endpoint) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &ExternalBackend{ |
||||
signers: []accounts.Wallet{signer}, |
||||
}, nil |
||||
} |
||||
|
||||
func (eb *ExternalBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { |
||||
return event.NewSubscription(func(quit <-chan struct{}) error { |
||||
<-quit |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// ExternalSigner provides an API to interact with an external signer (clef)
|
||||
// It proxies request to the external signer while forwarding relevant
|
||||
// request headers
|
||||
type ExternalSigner struct { |
||||
client *rpc.Client |
||||
endpoint string |
||||
status string |
||||
cacheMu sync.RWMutex |
||||
cache []accounts.Account |
||||
} |
||||
|
||||
func NewExternalSigner(endpoint string) (*ExternalSigner, error) { |
||||
client, err := rpc.Dial(endpoint) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
extsigner := &ExternalSigner{ |
||||
client: client, |
||||
endpoint: endpoint, |
||||
} |
||||
// Check if reachable
|
||||
version, err := extsigner.pingVersion() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
extsigner.status = fmt.Sprintf("ok [version=%v]", version) |
||||
return extsigner, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) URL() accounts.URL { |
||||
return accounts.URL{ |
||||
Scheme: "extapi", |
||||
Path: api.endpoint, |
||||
} |
||||
} |
||||
|
||||
func (api *ExternalSigner) Status() (string, error) { |
||||
return api.status, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) Open(passphrase string) error { |
||||
return fmt.Errorf("operation not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) Close() error { |
||||
return fmt.Errorf("operation not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) Accounts() []accounts.Account { |
||||
var accnts []accounts.Account |
||||
res, err := api.listAccounts() |
||||
if err != nil { |
||||
log.Error("account listing failed", "error", err) |
||||
return accnts |
||||
} |
||||
for _, addr := range res { |
||||
accnts = append(accnts, accounts.Account{ |
||||
URL: accounts.URL{ |
||||
Scheme: "extapi", |
||||
Path: api.endpoint, |
||||
}, |
||||
Address: addr, |
||||
}) |
||||
} |
||||
api.cacheMu.Lock() |
||||
api.cache = accnts |
||||
api.cacheMu.Unlock() |
||||
return accnts |
||||
} |
||||
|
||||
func (api *ExternalSigner) Contains(account accounts.Account) bool { |
||||
api.cacheMu.RLock() |
||||
defer api.cacheMu.RUnlock() |
||||
for _, a := range api.cache { |
||||
if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (api *ExternalSigner) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { |
||||
return accounts.Account{}, fmt.Errorf("operation not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) SelfDerive(bases []accounts.DerivationPath, chain ethereum.ChainStateReader) { |
||||
log.Error("operation SelfDerive not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) signHash(account accounts.Account, hash []byte) ([]byte, error) { |
||||
return []byte{}, fmt.Errorf("operation not supported on external signers") |
||||
} |
||||
|
||||
// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
|
||||
func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { |
||||
var res hexutil.Bytes |
||||
var signAddress = common.NewMixedcaseAddress(account.Address) |
||||
if err := api.client.Call(&res, "account_signData", |
||||
mimeType, |
||||
&signAddress, // Need to use the pointer here, because of how MarshalJSON is defined
|
||||
hexutil.Encode(data)); err != nil { |
||||
return nil, err |
||||
} |
||||
// If V is on 27/28-form, convert to to 0/1 for Clique
|
||||
if mimeType == accounts.MimetypeClique && (res[64] == 27 || res[64] == 28) { |
||||
res[64] -= 27 // Transform V from 27/28 to 0/1 for Clique use
|
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) SignText(account accounts.Account, text []byte) ([]byte, error) { |
||||
var res hexutil.Bytes |
||||
var signAddress = common.NewMixedcaseAddress(account.Address) |
||||
if err := api.client.Call(&res, "account_signData", |
||||
accounts.MimetypeTextPlain, |
||||
&signAddress, // Need to use the pointer here, because of how MarshalJSON is defined
|
||||
hexutil.Encode(text)); err != nil { |
||||
return nil, err |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
res := ethapi.SignTransactionResult{} |
||||
to := common.NewMixedcaseAddress(*tx.To()) |
||||
data := hexutil.Bytes(tx.Data()) |
||||
args := &core.SendTxArgs{ |
||||
Data: &data, |
||||
Nonce: hexutil.Uint64(tx.Nonce()), |
||||
Value: hexutil.Big(*tx.Value()), |
||||
Gas: hexutil.Uint64(tx.Gas()), |
||||
GasPrice: hexutil.Big(*tx.GasPrice()), |
||||
To: &to, |
||||
From: common.NewMixedcaseAddress(account.Address), |
||||
} |
||||
|
||||
if err := api.client.Call(&res, "account_signTransaction", args); err != nil { |
||||
return nil, err |
||||
} |
||||
return res.Tx, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { |
||||
return []byte{}, fmt.Errorf("passphrase-operations not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
return nil, fmt.Errorf("passphrase-operations not supported on external signers") |
||||
} |
||||
func (api *ExternalSigner) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { |
||||
return nil, fmt.Errorf("passphrase-operations not supported on external signers") |
||||
} |
||||
|
||||
func (api *ExternalSigner) listAccounts() ([]common.Address, error) { |
||||
var res []common.Address |
||||
if err := api.client.Call(&res, "account_list"); err != nil { |
||||
return nil, err |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
func (api *ExternalSigner) pingVersion() (string, error) { |
||||
var v string |
||||
if err := api.client.Call(&v, "account_version"); err != nil { |
||||
return "", err |
||||
} |
||||
return v, nil |
||||
} |
@ -0,0 +1,152 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"math" |
||||
"math/big" |
||||
"strings" |
||||
) |
||||
|
||||
// DefaultRootDerivationPath is the root path to which custom derivation endpoints
|
||||
// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
|
||||
// at m/44'/60'/0'/1, etc.
|
||||
var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} |
||||
|
||||
// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
|
||||
// are incremented. As such, the first account will be at m/44'/60'/0'/0/0, the second
|
||||
// at m/44'/60'/0'/0/1, etc.
|
||||
var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0} |
||||
|
||||
// LegacyLedgerBaseDerivationPath is the legacy base path from which custom derivation
|
||||
// endpoints are incremented. As such, the first account will be at m/44'/60'/0'/0, the
|
||||
// second at m/44'/60'/0'/1, etc.
|
||||
var LegacyLedgerBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} |
||||
|
||||
// DerivationPath represents the computer friendly version of a hierarchical
|
||||
// deterministic wallet account derivaion path.
|
||||
//
|
||||
// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
// defines derivation paths to be of the form:
|
||||
//
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
//
|
||||
// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
|
||||
// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
|
||||
// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
|
||||
// the `coin_type` 60' (or 0x8000003C) to Ethereum.
|
||||
//
|
||||
// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
|
||||
// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
|
||||
// yet whether accounts should increment the last component or the children of
|
||||
// that. We will go with the simpler approach of incrementing the last component.
|
||||
type DerivationPath []uint32 |
||||
|
||||
// ParseDerivationPath converts a user specified derivation path string to the
|
||||
// internal binary representation.
|
||||
//
|
||||
// Full derivation paths need to start with the `m/` prefix, relative derivation
|
||||
// paths (which will get appended to the default root path) must not have prefixes
|
||||
// in front of the first element. Whitespace is ignored.
|
||||
func ParseDerivationPath(path string) (DerivationPath, error) { |
||||
var result DerivationPath |
||||
|
||||
// Handle absolute or relative paths
|
||||
components := strings.Split(path, "/") |
||||
switch { |
||||
case len(components) == 0: |
||||
return nil, errors.New("empty derivation path") |
||||
|
||||
case strings.TrimSpace(components[0]) == "": |
||||
return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones") |
||||
|
||||
case strings.TrimSpace(components[0]) == "m": |
||||
components = components[1:] |
||||
|
||||
default: |
||||
result = append(result, DefaultRootDerivationPath...) |
||||
} |
||||
// All remaining components are relative, append one by one
|
||||
if len(components) == 0 { |
||||
return nil, errors.New("empty derivation path") // Empty relative paths
|
||||
} |
||||
for _, component := range components { |
||||
// Ignore any user added whitespace
|
||||
component = strings.TrimSpace(component) |
||||
var value uint32 |
||||
|
||||
// Handle hardened paths
|
||||
if strings.HasSuffix(component, "'") { |
||||
value = 0x80000000 |
||||
component = strings.TrimSpace(strings.TrimSuffix(component, "'")) |
||||
} |
||||
// Handle the non hardened component
|
||||
bigval, ok := new(big.Int).SetString(component, 0) |
||||
if !ok { |
||||
return nil, fmt.Errorf("invalid component: %s", component) |
||||
} |
||||
max := math.MaxUint32 - value |
||||
if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 { |
||||
if value == 0 { |
||||
return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max) |
||||
} |
||||
return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max) |
||||
} |
||||
value += uint32(bigval.Uint64()) |
||||
|
||||
// Append and repeat
|
||||
result = append(result, value) |
||||
} |
||||
return result, nil |
||||
} |
||||
|
||||
// String implements the stringer interface, converting a binary derivation path
|
||||
// to its canonical representation.
|
||||
func (path DerivationPath) String() string { |
||||
result := "m" |
||||
for _, component := range path { |
||||
var hardened bool |
||||
if component >= 0x80000000 { |
||||
component -= 0x80000000 |
||||
hardened = true |
||||
} |
||||
result = fmt.Sprintf("%s/%d", result, component) |
||||
if hardened { |
||||
result += "'" |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
|
||||
// MarshalJSON turns a derivation path into its json-serialized string
|
||||
func (path DerivationPath) MarshalJSON() ([]byte, error) { |
||||
return json.Marshal(path.String()) |
||||
} |
||||
|
||||
// UnmarshalJSON a json-serialized string back into a derivation path
|
||||
func (path *DerivationPath) UnmarshalJSON(b []byte) error { |
||||
var dp string |
||||
var err error |
||||
if err = json.Unmarshal(b, &dp); err != nil { |
||||
return err |
||||
} |
||||
*path, err = ParseDerivationPath(dp) |
||||
return err |
||||
} |
@ -0,0 +1,79 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
// Tests that HD derivation paths can be correctly parsed into our internal binary
|
||||
// representation.
|
||||
func TestHDPathParsing(t *testing.T) { |
||||
tests := []struct { |
||||
input string |
||||
output DerivationPath |
||||
}{ |
||||
// Plain absolute derivation paths
|
||||
{"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, |
||||
{"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, |
||||
{"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, |
||||
{"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, |
||||
{"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, |
||||
{"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, |
||||
|
||||
// Plain relative derivation paths
|
||||
{"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, |
||||
{"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, |
||||
{"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, |
||||
{"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, |
||||
{"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, |
||||
|
||||
// Hexadecimal absolute derivation paths
|
||||
{"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, |
||||
{"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, |
||||
{"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, |
||||
{"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, |
||||
{"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, |
||||
{"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, |
||||
|
||||
// Hexadecimal relative derivation paths
|
||||
{"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, |
||||
{"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, |
||||
{"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, |
||||
{"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, |
||||
{"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, |
||||
|
||||
// Weird inputs just to ensure they work
|
||||
{" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, |
||||
|
||||
// Invaid derivation paths
|
||||
{"", nil}, // Empty relative derivation path
|
||||
{"m", nil}, // Empty absolute derivation path
|
||||
{"m/", nil}, // Missing last derivation component
|
||||
{"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
|
||||
{"m/2147483648'", nil}, // Overflows 32 bit integer
|
||||
{"m/-1'", nil}, // Cannot contain negative number
|
||||
} |
||||
for i, tt := range tests { |
||||
if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) { |
||||
t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output) |
||||
} else if path == nil && err == nil { |
||||
t.Errorf("test %d: nil path and error: %v", i, err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,301 @@ |
||||
// 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 keystore |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"sort" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
mapset "github.com/deckarep/golang-set" |
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// Minimum amount of time between cache reloads. This limit applies if the platform does
|
||||
// not support change notifications. It also applies if the keystore directory does not
|
||||
// exist yet, the code will attempt to create a watcher at most this often.
|
||||
const minReloadInterval = 2 * time.Second |
||||
|
||||
type accountsByURL []accounts.Account |
||||
|
||||
func (s accountsByURL) Len() int { return len(s) } |
||||
func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 } |
||||
func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
||||
|
||||
// AmbiguousAddrError is returned when attempting to unlock
|
||||
// an address for which more than one file exists.
|
||||
type AmbiguousAddrError struct { |
||||
Addr common.Address |
||||
Matches []accounts.Account |
||||
} |
||||
|
||||
func (err *AmbiguousAddrError) Error() string { |
||||
files := "" |
||||
for i, a := range err.Matches { |
||||
files += a.URL.Path |
||||
if i < len(err.Matches)-1 { |
||||
files += ", " |
||||
} |
||||
} |
||||
return fmt.Sprintf("multiple keys match address (%s)", files) |
||||
} |
||||
|
||||
// accountCache is a live index of all accounts in the keystore.
|
||||
type accountCache struct { |
||||
keydir string |
||||
watcher *watcher |
||||
mu sync.Mutex |
||||
all accountsByURL |
||||
byAddr map[common.Address][]accounts.Account |
||||
throttle *time.Timer |
||||
notify chan struct{} |
||||
fileC fileCache |
||||
} |
||||
|
||||
func newAccountCache(keydir string) (*accountCache, chan struct{}) { |
||||
ac := &accountCache{ |
||||
keydir: keydir, |
||||
byAddr: make(map[common.Address][]accounts.Account), |
||||
notify: make(chan struct{}, 1), |
||||
fileC: fileCache{all: mapset.NewThreadUnsafeSet()}, |
||||
} |
||||
ac.watcher = newWatcher(ac) |
||||
return ac, ac.notify |
||||
} |
||||
|
||||
func (ac *accountCache) accounts() []accounts.Account { |
||||
ac.maybeReload() |
||||
ac.mu.Lock() |
||||
defer ac.mu.Unlock() |
||||
cpy := make([]accounts.Account, len(ac.all)) |
||||
copy(cpy, ac.all) |
||||
return cpy |
||||
} |
||||
|
||||
func (ac *accountCache) hasAddress(addr common.Address) bool { |
||||
ac.maybeReload() |
||||
ac.mu.Lock() |
||||
defer ac.mu.Unlock() |
||||
return len(ac.byAddr[addr]) > 0 |
||||
} |
||||
|
||||
func (ac *accountCache) add(newAccount accounts.Account) { |
||||
ac.mu.Lock() |
||||
defer ac.mu.Unlock() |
||||
|
||||
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) |
||||
if i < len(ac.all) && ac.all[i] == newAccount { |
||||
return |
||||
} |
||||
// newAccount is not in the cache.
|
||||
ac.all = append(ac.all, accounts.Account{}) |
||||
copy(ac.all[i+1:], ac.all[i:]) |
||||
ac.all[i] = newAccount |
||||
ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) |
||||
} |
||||
|
||||
// note: removed needs to be unique here (i.e. both File and Address must be set).
|
||||
func (ac *accountCache) delete(removed accounts.Account) { |
||||
ac.mu.Lock() |
||||
defer ac.mu.Unlock() |
||||
|
||||
ac.all = removeAccount(ac.all, removed) |
||||
if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { |
||||
delete(ac.byAddr, removed.Address) |
||||
} else { |
||||
ac.byAddr[removed.Address] = ba |
||||
} |
||||
} |
||||
|
||||
// deleteByFile removes an account referenced by the given path.
|
||||
func (ac *accountCache) deleteByFile(path string) { |
||||
ac.mu.Lock() |
||||
defer ac.mu.Unlock() |
||||
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Path >= path }) |
||||
|
||||
if i < len(ac.all) && ac.all[i].URL.Path == path { |
||||
removed := ac.all[i] |
||||
ac.all = append(ac.all[:i], ac.all[i+1:]...) |
||||
if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { |
||||
delete(ac.byAddr, removed.Address) |
||||
} else { |
||||
ac.byAddr[removed.Address] = ba |
||||
} |
||||
} |
||||
} |
||||
|
||||
func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { |
||||
for i := range slice { |
||||
if slice[i] == elem { |
||||
return append(slice[:i], slice[i+1:]...) |
||||
} |
||||
} |
||||
return slice |
||||
} |
||||
|
||||
// find returns the cached account for address if there is a unique match.
|
||||
// The exact matching rules are explained by the documentation of accounts.Account.
|
||||
// Callers must hold ac.mu.
|
||||
func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { |
||||
// Limit search to address candidates if possible.
|
||||
matches := ac.all |
||||
if (a.Address != common.Address{}) { |
||||
matches = ac.byAddr[a.Address] |
||||
} |
||||
if a.URL.Path != "" { |
||||
// If only the basename is specified, complete the path.
|
||||
if !strings.ContainsRune(a.URL.Path, filepath.Separator) { |
||||
a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) |
||||
} |
||||
for i := range matches { |
||||
if matches[i].URL == a.URL { |
||||
return matches[i], nil |
||||
} |
||||
} |
||||
if (a.Address == common.Address{}) { |
||||
return accounts.Account{}, ErrNoMatch |
||||
} |
||||
} |
||||
switch len(matches) { |
||||
case 1: |
||||
return matches[0], nil |
||||
case 0: |
||||
return accounts.Account{}, ErrNoMatch |
||||
default: |
||||
err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} |
||||
copy(err.Matches, matches) |
||||
sort.Sort(accountsByURL(err.Matches)) |
||||
return accounts.Account{}, err |
||||
} |
||||
} |
||||
|
||||
func (ac *accountCache) maybeReload() { |
||||
ac.mu.Lock() |
||||
|
||||
if ac.watcher.running { |
||||
ac.mu.Unlock() |
||||
return // A watcher is running and will keep the cache up-to-date.
|
||||
} |
||||
if ac.throttle == nil { |
||||
ac.throttle = time.NewTimer(0) |
||||
} else { |
||||
select { |
||||
case <-ac.throttle.C: |
||||
default: |
||||
ac.mu.Unlock() |
||||
return // The cache was reloaded recently.
|
||||
} |
||||
} |
||||
// No watcher running, start it.
|
||||
ac.watcher.start() |
||||
ac.throttle.Reset(minReloadInterval) |
||||
ac.mu.Unlock() |
||||
ac.scanAccounts() |
||||
} |
||||
|
||||
func (ac *accountCache) close() { |
||||
ac.mu.Lock() |
||||
ac.watcher.close() |
||||
if ac.throttle != nil { |
||||
ac.throttle.Stop() |
||||
} |
||||
if ac.notify != nil { |
||||
close(ac.notify) |
||||
ac.notify = nil |
||||
} |
||||
ac.mu.Unlock() |
||||
} |
||||
|
||||
// scanAccounts checks if any changes have occurred on the filesystem, and
|
||||
// updates the account cache accordingly
|
||||
func (ac *accountCache) scanAccounts() error { |
||||
// Scan the entire folder metadata for file changes
|
||||
creates, deletes, updates, err := ac.fileC.scan(ac.keydir) |
||||
if err != nil { |
||||
log.Debug("Failed to reload keystore contents", "err", err) |
||||
return err |
||||
} |
||||
if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { |
||||
return nil |
||||
} |
||||
// Create a helper method to scan the contents of the key files
|
||||
var ( |
||||
buf = new(bufio.Reader) |
||||
key struct { |
||||
Address string `json:"address"` |
||||
} |
||||
) |
||||
readAccount := func(path string) *accounts.Account { |
||||
fd, err := os.Open(path) |
||||
if err != nil { |
||||
log.Trace("Failed to open keystore file", "path", path, "err", err) |
||||
return nil |
||||
} |
||||
defer fd.Close() |
||||
buf.Reset(fd) |
||||
// Parse the address.
|
||||
key.Address = "" |
||||
err = json.NewDecoder(buf).Decode(&key) |
||||
addr := common.HexToAddress(key.Address) |
||||
switch { |
||||
case err != nil: |
||||
log.Debug("Failed to decode keystore key", "path", path, "err", err) |
||||
case (addr == common.Address{}): |
||||
log.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") |
||||
default: |
||||
return &accounts.Account{ |
||||
Address: addr, |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}, |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
// Process all the file diffs
|
||||
start := time.Now() |
||||
|
||||
for _, p := range creates.ToSlice() { |
||||
if a := readAccount(p.(string)); a != nil { |
||||
ac.add(*a) |
||||
} |
||||
} |
||||
for _, p := range deletes.ToSlice() { |
||||
ac.deleteByFile(p.(string)) |
||||
} |
||||
for _, p := range updates.ToSlice() { |
||||
path := p.(string) |
||||
ac.deleteByFile(path) |
||||
if a := readAccount(path); a != nil { |
||||
ac.add(*a) |
||||
} |
||||
} |
||||
end := time.Now() |
||||
|
||||
select { |
||||
case ac.notify <- struct{}{}: |
||||
default: |
||||
} |
||||
log.Trace("Handled keystore changes", "time", end.Sub(start)) |
||||
return nil |
||||
} |
@ -0,0 +1,406 @@ |
||||
// 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 keystore |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/ioutil" |
||||
"math/rand" |
||||
"os" |
||||
"path/filepath" |
||||
"reflect" |
||||
"sort" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/cespare/cp" |
||||
"github.com/davecgh/go-spew/spew" |
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
var ( |
||||
cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) |
||||
cachetestAccounts = []accounts.Account{ |
||||
{ |
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, |
||||
}, |
||||
} |
||||
) |
||||
|
||||
func TestWatchNewFile(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
dir, ks := tmpKeyStore(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
// Ensure the watcher is started before adding any files.
|
||||
ks.Accounts() |
||||
time.Sleep(1000 * time.Millisecond) |
||||
|
||||
// Move in the files.
|
||||
wantAccounts := make([]accounts.Account, len(cachetestAccounts)) |
||||
for i := range cachetestAccounts { |
||||
wantAccounts[i] = accounts.Account{ |
||||
Address: cachetestAccounts[i].Address, |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, |
||||
} |
||||
if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
// ks should see the accounts.
|
||||
var list []accounts.Account |
||||
for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { |
||||
list = ks.Accounts() |
||||
if reflect.DeepEqual(list, wantAccounts) { |
||||
// ks should have also received change notifications
|
||||
select { |
||||
case <-ks.changes: |
||||
default: |
||||
t.Fatalf("wasn't notified of new accounts") |
||||
} |
||||
return |
||||
} |
||||
time.Sleep(d) |
||||
} |
||||
t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts)) |
||||
} |
||||
|
||||
func TestWatchNoDir(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
// Create ks but not the directory that it watches.
|
||||
rand.Seed(time.Now().UnixNano()) |
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) |
||||
ks := NewKeyStore(dir, LightScryptN, LightScryptP) |
||||
|
||||
list := ks.Accounts() |
||||
if len(list) > 0 { |
||||
t.Error("initial account list not empty:", list) |
||||
} |
||||
time.Sleep(100 * time.Millisecond) |
||||
|
||||
// Create the directory and copy a key file into it.
|
||||
os.MkdirAll(dir, 0700) |
||||
defer os.RemoveAll(dir) |
||||
file := filepath.Join(dir, "aaa") |
||||
if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// ks should see the account.
|
||||
wantAccounts := []accounts.Account{cachetestAccounts[0]} |
||||
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} |
||||
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { |
||||
list = ks.Accounts() |
||||
if reflect.DeepEqual(list, wantAccounts) { |
||||
// ks should have also received change notifications
|
||||
select { |
||||
case <-ks.changes: |
||||
default: |
||||
t.Fatalf("wasn't notified of new accounts") |
||||
} |
||||
return |
||||
} |
||||
time.Sleep(d) |
||||
} |
||||
t.Errorf("\ngot %v\nwant %v", list, wantAccounts) |
||||
} |
||||
|
||||
func TestCacheInitialReload(t *testing.T) { |
||||
cache, _ := newAccountCache(cachetestDir) |
||||
accounts := cache.accounts() |
||||
if !reflect.DeepEqual(accounts, cachetestAccounts) { |
||||
t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) |
||||
} |
||||
} |
||||
|
||||
func TestCacheAddDeleteOrder(t *testing.T) { |
||||
cache, _ := newAccountCache("testdata/no-such-dir") |
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accs := []accounts.Account{ |
||||
{ |
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, |
||||
}, |
||||
} |
||||
for _, a := range accs { |
||||
cache.add(a) |
||||
} |
||||
// Add some of them twice to check that they don't get reinserted.
|
||||
cache.add(accs[0]) |
||||
cache.add(accs[2]) |
||||
|
||||
// Check that the account list is sorted by filename.
|
||||
wantAccounts := make([]accounts.Account, len(accs)) |
||||
copy(wantAccounts, accs) |
||||
sort.Sort(accountsByURL(wantAccounts)) |
||||
list := cache.accounts() |
||||
if !reflect.DeepEqual(list, wantAccounts) { |
||||
t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) |
||||
} |
||||
for _, a := range accs { |
||||
if !cache.hasAddress(a.Address) { |
||||
t.Errorf("expected hasAccount(%x) to return true", a.Address) |
||||
} |
||||
} |
||||
if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { |
||||
t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) |
||||
} |
||||
|
||||
// Delete a few keys from the cache.
|
||||
for i := 0; i < len(accs); i += 2 { |
||||
cache.delete(wantAccounts[i]) |
||||
} |
||||
cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) |
||||
|
||||
// Check content again after deletion.
|
||||
wantAccountsAfterDelete := []accounts.Account{ |
||||
wantAccounts[1], |
||||
wantAccounts[3], |
||||
wantAccounts[5], |
||||
} |
||||
list = cache.accounts() |
||||
if !reflect.DeepEqual(list, wantAccountsAfterDelete) { |
||||
t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) |
||||
} |
||||
for _, a := range wantAccountsAfterDelete { |
||||
if !cache.hasAddress(a.Address) { |
||||
t.Errorf("expected hasAccount(%x) to return true", a.Address) |
||||
} |
||||
} |
||||
if cache.hasAddress(wantAccounts[0].Address) { |
||||
t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) |
||||
} |
||||
} |
||||
|
||||
func TestCacheFind(t *testing.T) { |
||||
dir := filepath.Join("testdata", "dir") |
||||
cache, _ := newAccountCache(dir) |
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accs := []accounts.Account{ |
||||
{ |
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, |
||||
}, |
||||
{ |
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, |
||||
}, |
||||
} |
||||
for _, a := range accs { |
||||
cache.add(a) |
||||
} |
||||
|
||||
nomatchAccount := accounts.Account{ |
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, |
||||
} |
||||
tests := []struct { |
||||
Query accounts.Account |
||||
WantResult accounts.Account |
||||
WantError error |
||||
}{ |
||||
// by address
|
||||
{Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, |
||||
// by file
|
||||
{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, |
||||
// by basename
|
||||
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, |
||||
// by file and address
|
||||
{Query: accs[0], WantResult: accs[0]}, |
||||
// ambiguous address, tie resolved by file
|
||||
{Query: accs[2], WantResult: accs[2]}, |
||||
// ambiguous address error
|
||||
{ |
||||
Query: accounts.Account{Address: accs[2].Address}, |
||||
WantError: &AmbiguousAddrError{ |
||||
Addr: accs[2].Address, |
||||
Matches: []accounts.Account{accs[2], accs[3]}, |
||||
}, |
||||
}, |
||||
// no match error
|
||||
{Query: nomatchAccount, WantError: ErrNoMatch}, |
||||
{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, |
||||
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch}, |
||||
{Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, |
||||
} |
||||
for i, test := range tests { |
||||
a, err := cache.find(test.Query) |
||||
if !reflect.DeepEqual(err, test.WantError) { |
||||
t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) |
||||
continue |
||||
} |
||||
if a != test.WantResult { |
||||
t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) |
||||
continue |
||||
} |
||||
} |
||||
} |
||||
|
||||
func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { |
||||
var list []accounts.Account |
||||
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { |
||||
list = ks.Accounts() |
||||
if reflect.DeepEqual(list, wantAccounts) { |
||||
// ks should have also received change notifications
|
||||
select { |
||||
case <-ks.changes: |
||||
default: |
||||
return fmt.Errorf("wasn't notified of new accounts") |
||||
} |
||||
return nil |
||||
} |
||||
time.Sleep(d) |
||||
} |
||||
return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) |
||||
} |
||||
|
||||
// TestUpdatedKeyfileContents tests that updating the contents of a keystore file
|
||||
// is noticed by the watcher, and the account cache is updated accordingly
|
||||
func TestUpdatedKeyfileContents(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
// Create a temporary kesytore to test with
|
||||
rand.Seed(time.Now().UnixNano()) |
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) |
||||
ks := NewKeyStore(dir, LightScryptN, LightScryptP) |
||||
|
||||
list := ks.Accounts() |
||||
if len(list) > 0 { |
||||
t.Error("initial account list not empty:", list) |
||||
} |
||||
time.Sleep(100 * time.Millisecond) |
||||
|
||||
// Create the directory and copy a key file into it.
|
||||
os.MkdirAll(dir, 0700) |
||||
defer os.RemoveAll(dir) |
||||
file := filepath.Join(dir, "aaa") |
||||
|
||||
// Place one of our testfiles in there
|
||||
if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// ks should see the account.
|
||||
wantAccounts := []accounts.Account{cachetestAccounts[0]} |
||||
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} |
||||
if err := waitForAccounts(wantAccounts, ks); err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
|
||||
// needed so that modTime of `file` is different to its current value after forceCopyFile
|
||||
time.Sleep(1000 * time.Millisecond) |
||||
|
||||
// Now replace file contents
|
||||
if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { |
||||
t.Fatal(err) |
||||
return |
||||
} |
||||
wantAccounts = []accounts.Account{cachetestAccounts[1]} |
||||
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} |
||||
if err := waitForAccounts(wantAccounts, ks); err != nil { |
||||
t.Errorf("First replacement failed") |
||||
t.Error(err) |
||||
return |
||||
} |
||||
|
||||
// needed so that modTime of `file` is different to its current value after forceCopyFile
|
||||
time.Sleep(1000 * time.Millisecond) |
||||
|
||||
// Now replace file contents again
|
||||
if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { |
||||
t.Fatal(err) |
||||
return |
||||
} |
||||
wantAccounts = []accounts.Account{cachetestAccounts[2]} |
||||
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} |
||||
if err := waitForAccounts(wantAccounts, ks); err != nil { |
||||
t.Errorf("Second replacement failed") |
||||
t.Error(err) |
||||
return |
||||
} |
||||
|
||||
// needed so that modTime of `file` is different to its current value after ioutil.WriteFile
|
||||
time.Sleep(1000 * time.Millisecond) |
||||
|
||||
// Now replace file contents with crap
|
||||
if err := ioutil.WriteFile(file, []byte("foo"), 0644); err != nil { |
||||
t.Fatal(err) |
||||
return |
||||
} |
||||
if err := waitForAccounts([]accounts.Account{}, ks); err != nil { |
||||
t.Errorf("Emptying account file failed") |
||||
t.Error(err) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// forceCopyFile is like cp.CopyFile, but doesn't complain if the destination exists.
|
||||
func forceCopyFile(dst, src string) error { |
||||
data, err := ioutil.ReadFile(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ioutil.WriteFile(dst, data, 0644) |
||||
} |
@ -0,0 +1,102 @@ |
||||
// 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 keystore |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
mapset "github.com/deckarep/golang-set" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// fileCache is a cache of files seen during scan of keystore.
|
||||
type fileCache struct { |
||||
all mapset.Set // Set of all files from the keystore folder
|
||||
lastMod time.Time // Last time instance when a file was modified
|
||||
mu sync.RWMutex |
||||
} |
||||
|
||||
// scan performs a new scan on the given directory, compares against the already
|
||||
// cached filenames, and returns file sets: creates, deletes, updates.
|
||||
func (fc *fileCache) scan(keyDir string) (mapset.Set, mapset.Set, mapset.Set, error) { |
||||
t0 := time.Now() |
||||
|
||||
// List all the failes from the keystore folder
|
||||
files, err := ioutil.ReadDir(keyDir) |
||||
if err != nil { |
||||
return nil, nil, nil, err |
||||
} |
||||
t1 := time.Now() |
||||
|
||||
fc.mu.Lock() |
||||
defer fc.mu.Unlock() |
||||
|
||||
// Iterate all the files and gather their metadata
|
||||
all := mapset.NewThreadUnsafeSet() |
||||
mods := mapset.NewThreadUnsafeSet() |
||||
|
||||
var newLastMod time.Time |
||||
for _, fi := range files { |
||||
path := filepath.Join(keyDir, fi.Name()) |
||||
// Skip any non-key files from the folder
|
||||
if nonKeyFile(fi) { |
||||
log.Trace("Ignoring file on account scan", "path", path) |
||||
continue |
||||
} |
||||
// Gather the set of all and fresly modified files
|
||||
all.Add(path) |
||||
|
||||
modified := fi.ModTime() |
||||
if modified.After(fc.lastMod) { |
||||
mods.Add(path) |
||||
} |
||||
if modified.After(newLastMod) { |
||||
newLastMod = modified |
||||
} |
||||
} |
||||
t2 := time.Now() |
||||
|
||||
// Update the tracked files and return the three sets
|
||||
deletes := fc.all.Difference(all) // Deletes = previous - current
|
||||
creates := all.Difference(fc.all) // Creates = current - previous
|
||||
updates := mods.Difference(creates) // Updates = modified - creates
|
||||
|
||||
fc.all, fc.lastMod = all, newLastMod |
||||
t3 := time.Now() |
||||
|
||||
// Report on the scanning stats and return
|
||||
log.Debug("FS scan times", "list", t1.Sub(t0), "set", t2.Sub(t1), "diff", t3.Sub(t2)) |
||||
return creates, deletes, updates, nil |
||||
} |
||||
|
||||
// nonKeyFile ignores editor backups, hidden files and folders/symlinks.
|
||||
func nonKeyFile(fi os.FileInfo) bool { |
||||
// Skip editor backups and UNIX-style hidden files.
|
||||
if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { |
||||
return true |
||||
} |
||||
// Skip misc special files, directories (yes, symlinks too).
|
||||
if fi.IsDir() || fi.Mode()&os.ModeType != 0 { |
||||
return true |
||||
} |
||||
return false |
||||
} |
@ -0,0 +1,232 @@ |
||||
// Copyright 2014 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 keystore |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/ecdsa" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/pborman/uuid" |
||||
) |
||||
|
||||
const ( |
||||
version = 3 |
||||
) |
||||
|
||||
type Key struct { |
||||
Id uuid.UUID // Version 4 "random" for unique id not derived from key data
|
||||
// to simplify lookups we also store the address
|
||||
Address common.Address |
||||
// we only store privkey as pubkey/address can be derived from it
|
||||
// privkey in this struct is always in plaintext
|
||||
PrivateKey *ecdsa.PrivateKey |
||||
} |
||||
|
||||
type keyStore interface { |
||||
// Loads and decrypts the key from disk.
|
||||
GetKey(addr common.Address, filename string, auth string) (*Key, error) |
||||
// Writes and encrypts the key.
|
||||
StoreKey(filename string, k *Key, auth string) error |
||||
// Joins filename with the key directory unless it is already absolute.
|
||||
JoinPath(filename string) string |
||||
} |
||||
|
||||
type plainKeyJSON struct { |
||||
Address string `json:"address"` |
||||
PrivateKey string `json:"privatekey"` |
||||
Id string `json:"id"` |
||||
Version int `json:"version"` |
||||
} |
||||
|
||||
type encryptedKeyJSONV3 struct { |
||||
Address string `json:"address"` |
||||
Crypto CryptoJSON `json:"crypto"` |
||||
Id string `json:"id"` |
||||
Version int `json:"version"` |
||||
} |
||||
|
||||
type encryptedKeyJSONV1 struct { |
||||
Address string `json:"address"` |
||||
Crypto CryptoJSON `json:"crypto"` |
||||
Id string `json:"id"` |
||||
Version string `json:"version"` |
||||
} |
||||
|
||||
type CryptoJSON struct { |
||||
Cipher string `json:"cipher"` |
||||
CipherText string `json:"ciphertext"` |
||||
CipherParams cipherparamsJSON `json:"cipherparams"` |
||||
KDF string `json:"kdf"` |
||||
KDFParams map[string]interface{} `json:"kdfparams"` |
||||
MAC string `json:"mac"` |
||||
} |
||||
|
||||
type cipherparamsJSON struct { |
||||
IV string `json:"iv"` |
||||
} |
||||
|
||||
func (k *Key) MarshalJSON() (j []byte, err error) { |
||||
jStruct := plainKeyJSON{ |
||||
hex.EncodeToString(k.Address[:]), |
||||
hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)), |
||||
k.Id.String(), |
||||
version, |
||||
} |
||||
j, err = json.Marshal(jStruct) |
||||
return j, err |
||||
} |
||||
|
||||
func (k *Key) UnmarshalJSON(j []byte) (err error) { |
||||
keyJSON := new(plainKeyJSON) |
||||
err = json.Unmarshal(j, &keyJSON) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
u := new(uuid.UUID) |
||||
*u = uuid.Parse(keyJSON.Id) |
||||
k.Id = *u |
||||
addr, err := hex.DecodeString(keyJSON.Address) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
privkey, err := crypto.HexToECDSA(keyJSON.PrivateKey) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
k.Address = common.BytesToAddress(addr) |
||||
k.PrivateKey = privkey |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { |
||||
id := uuid.NewRandom() |
||||
key := &Key{ |
||||
Id: id, |
||||
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), |
||||
PrivateKey: privateKeyECDSA, |
||||
} |
||||
return key |
||||
} |
||||
|
||||
// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit
|
||||
// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we
|
||||
// retry until the first byte is 0.
|
||||
func NewKeyForDirectICAP(rand io.Reader) *Key { |
||||
randBytes := make([]byte, 64) |
||||
_, err := rand.Read(randBytes) |
||||
if err != nil { |
||||
panic("key generation: could not read from random source: " + err.Error()) |
||||
} |
||||
reader := bytes.NewReader(randBytes) |
||||
privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), reader) |
||||
if err != nil { |
||||
panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) |
||||
} |
||||
key := newKeyFromECDSA(privateKeyECDSA) |
||||
if !strings.HasPrefix(key.Address.Hex(), "0x00") { |
||||
return NewKeyForDirectICAP(rand) |
||||
} |
||||
return key |
||||
} |
||||
|
||||
func newKey(rand io.Reader) (*Key, error) { |
||||
privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return newKeyFromECDSA(privateKeyECDSA), nil |
||||
} |
||||
|
||||
func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { |
||||
key, err := newKey(rand) |
||||
if err != nil { |
||||
return nil, accounts.Account{}, err |
||||
} |
||||
a := accounts.Account{ |
||||
Address: key.Address, |
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}, |
||||
} |
||||
if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { |
||||
zeroKey(key.PrivateKey) |
||||
return nil, a, err |
||||
} |
||||
return key, a, err |
||||
} |
||||
|
||||
func writeTemporaryKeyFile(file string, content []byte) (string, error) { |
||||
// Create the keystore directory with appropriate permissions
|
||||
// in case it is not present yet.
|
||||
const dirPerm = 0700 |
||||
if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { |
||||
return "", err |
||||
} |
||||
// Atomic write: create a temporary hidden file first
|
||||
// then move it into place. TempFile assigns mode 0600.
|
||||
f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
if _, err := f.Write(content); err != nil { |
||||
f.Close() |
||||
os.Remove(f.Name()) |
||||
return "", err |
||||
} |
||||
f.Close() |
||||
return f.Name(), nil |
||||
} |
||||
|
||||
func writeKeyFile(file string, content []byte) error { |
||||
name, err := writeTemporaryKeyFile(file, content) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return os.Rename(name, file) |
||||
} |
||||
|
||||
// keyFileName implements the naming convention for keyfiles:
|
||||
// UTC--<created_at UTC ISO8601>-<address hex>
|
||||
func keyFileName(keyAddr common.Address) string { |
||||
ts := time.Now().UTC() |
||||
return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) |
||||
} |
||||
|
||||
func toISO8601(t time.Time) string { |
||||
var tz string |
||||
name, offset := t.Zone() |
||||
if name == "UTC" { |
||||
tz = "Z" |
||||
} else { |
||||
tz = fmt.Sprintf("%03d00", offset/3600) |
||||
} |
||||
return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", |
||||
t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) |
||||
} |
@ -0,0 +1,495 @@ |
||||
// 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 keystore implements encrypted storage of secp256k1 private keys.
|
||||
//
|
||||
// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
|
||||
// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
|
||||
package keystore |
||||
|
||||
import ( |
||||
"crypto/ecdsa" |
||||
crand "crypto/rand" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"os" |
||||
"path/filepath" |
||||
"reflect" |
||||
"runtime" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
) |
||||
|
||||
var ( |
||||
ErrLocked = accounts.NewAuthNeededError("password or unlock") |
||||
ErrNoMatch = errors.New("no key for given address or file") |
||||
ErrDecrypt = errors.New("could not decrypt key with given passphrase") |
||||
) |
||||
|
||||
// KeyStoreType is the reflect type of a keystore backend.
|
||||
var KeyStoreType = reflect.TypeOf(&KeyStore{}) |
||||
|
||||
// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
const KeyStoreScheme = "keystore" |
||||
|
||||
// Maximum time between wallet refreshes (if filesystem notifications don't work).
|
||||
const walletRefreshCycle = 3 * time.Second |
||||
|
||||
// KeyStore manages a key storage directory on disk.
|
||||
type KeyStore struct { |
||||
storage keyStore // Storage backend, might be cleartext or encrypted
|
||||
cache *accountCache // In-memory account cache over the filesystem storage
|
||||
changes chan struct{} // Channel receiving change notifications from the cache
|
||||
unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys)
|
||||
|
||||
wallets []accounts.Wallet // Wallet wrappers around the individual key files
|
||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||
updating bool // Whether the event notification loop is running
|
||||
|
||||
mu sync.RWMutex |
||||
} |
||||
|
||||
type unlocked struct { |
||||
*Key |
||||
abort chan struct{} |
||||
} |
||||
|
||||
// NewKeyStore creates a keystore for the given directory.
|
||||
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { |
||||
keydir, _ = filepath.Abs(keydir) |
||||
ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP, false}} |
||||
ks.init(keydir) |
||||
return ks |
||||
} |
||||
|
||||
// NewPlaintextKeyStore creates a keystore for the given directory.
|
||||
// Deprecated: Use NewKeyStore.
|
||||
func NewPlaintextKeyStore(keydir string) *KeyStore { |
||||
keydir, _ = filepath.Abs(keydir) |
||||
ks := &KeyStore{storage: &keyStorePlain{keydir}} |
||||
ks.init(keydir) |
||||
return ks |
||||
} |
||||
|
||||
func (ks *KeyStore) init(keydir string) { |
||||
// Lock the mutex since the account cache might call back with events
|
||||
ks.mu.Lock() |
||||
defer ks.mu.Unlock() |
||||
|
||||
// Initialize the set of unlocked keys and the account cache
|
||||
ks.unlocked = make(map[common.Address]*unlocked) |
||||
ks.cache, ks.changes = newAccountCache(keydir) |
||||
|
||||
// TODO: In order for this finalizer to work, there must be no references
|
||||
// to ks. addressCache doesn't keep a reference but unlocked keys do,
|
||||
// so the finalizer will not trigger until all timed unlocks have expired.
|
||||
runtime.SetFinalizer(ks, func(m *KeyStore) { |
||||
m.cache.close() |
||||
}) |
||||
// Create the initial list of wallets from the cache
|
||||
accs := ks.cache.accounts() |
||||
ks.wallets = make([]accounts.Wallet, len(accs)) |
||||
for i := 0; i < len(accs); i++ { |
||||
ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks} |
||||
} |
||||
} |
||||
|
||||
// Wallets implements accounts.Backend, returning all single-key wallets from the
|
||||
// keystore directory.
|
||||
func (ks *KeyStore) Wallets() []accounts.Wallet { |
||||
// Make sure the list of wallets is in sync with the account cache
|
||||
ks.refreshWallets() |
||||
|
||||
ks.mu.RLock() |
||||
defer ks.mu.RUnlock() |
||||
|
||||
cpy := make([]accounts.Wallet, len(ks.wallets)) |
||||
copy(cpy, ks.wallets) |
||||
return cpy |
||||
} |
||||
|
||||
// refreshWallets retrieves the current account list and based on that does any
|
||||
// necessary wallet refreshes.
|
||||
func (ks *KeyStore) refreshWallets() { |
||||
// Retrieve the current list of accounts
|
||||
ks.mu.Lock() |
||||
accs := ks.cache.accounts() |
||||
|
||||
// Transform the current list of wallets into the new one
|
||||
var ( |
||||
wallets = make([]accounts.Wallet, 0, len(accs)) |
||||
events []accounts.WalletEvent |
||||
) |
||||
|
||||
for _, account := range accs { |
||||
// Drop wallets while they were in front of the next account
|
||||
for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { |
||||
events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Kind: accounts.WalletDropped}) |
||||
ks.wallets = ks.wallets[1:] |
||||
} |
||||
// If there are no more wallets or the account is before the next, wrap new wallet
|
||||
if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { |
||||
wallet := &keystoreWallet{account: account, keystore: ks} |
||||
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) |
||||
wallets = append(wallets, wallet) |
||||
continue |
||||
} |
||||
// If the account is the same as the first wallet, keep it
|
||||
if ks.wallets[0].Accounts()[0] == account { |
||||
wallets = append(wallets, ks.wallets[0]) |
||||
ks.wallets = ks.wallets[1:] |
||||
continue |
||||
} |
||||
} |
||||
// Drop any leftover wallets and set the new batch
|
||||
for _, wallet := range ks.wallets { |
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) |
||||
} |
||||
ks.wallets = wallets |
||||
ks.mu.Unlock() |
||||
|
||||
// Fire all wallet events and return
|
||||
for _, event := range events { |
||||
ks.updateFeed.Send(event) |
||||
} |
||||
} |
||||
|
||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||
// receive notifications on the addition or removal of keystore wallets.
|
||||
func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { |
||||
// We need the mutex to reliably start/stop the update loop
|
||||
ks.mu.Lock() |
||||
defer ks.mu.Unlock() |
||||
|
||||
// Subscribe the caller and track the subscriber count
|
||||
sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) |
||||
|
||||
// Subscribers require an active notification loop, start it
|
||||
if !ks.updating { |
||||
ks.updating = true |
||||
go ks.updater() |
||||
} |
||||
return sub |
||||
} |
||||
|
||||
// updater is responsible for maintaining an up-to-date list of wallets stored in
|
||||
// the keystore, and for firing wallet addition/removal events. It listens for
|
||||
// account change events from the underlying account cache, and also periodically
|
||||
// forces a manual refresh (only triggers for systems where the filesystem notifier
|
||||
// is not running).
|
||||
func (ks *KeyStore) updater() { |
||||
for { |
||||
// Wait for an account update or a refresh timeout
|
||||
select { |
||||
case <-ks.changes: |
||||
case <-time.After(walletRefreshCycle): |
||||
} |
||||
// Run the wallet refresher
|
||||
ks.refreshWallets() |
||||
|
||||
// If all our subscribers left, stop the updater
|
||||
ks.mu.Lock() |
||||
if ks.updateScope.Count() == 0 { |
||||
ks.updating = false |
||||
ks.mu.Unlock() |
||||
return |
||||
} |
||||
ks.mu.Unlock() |
||||
} |
||||
} |
||||
|
||||
// HasAddress reports whether a key with the given address is present.
|
||||
func (ks *KeyStore) HasAddress(addr common.Address) bool { |
||||
return ks.cache.hasAddress(addr) |
||||
} |
||||
|
||||
// Accounts returns all key files present in the directory.
|
||||
func (ks *KeyStore) Accounts() []accounts.Account { |
||||
return ks.cache.accounts() |
||||
} |
||||
|
||||
// Delete deletes the key matched by account if the passphrase is correct.
|
||||
// If the account contains no filename, the address must match a unique key.
|
||||
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { |
||||
// Decrypting the key isn't really necessary, but we do
|
||||
// it anyway to check the password and zero out the key
|
||||
// immediately afterwards.
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if key != nil { |
||||
zeroKey(key.PrivateKey) |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// The order is crucial here. The key is dropped from the
|
||||
// cache after the file is gone so that a reload happening in
|
||||
// between won't insert it into the cache again.
|
||||
err = os.Remove(a.URL.Path) |
||||
if err == nil { |
||||
ks.cache.delete(a) |
||||
ks.refreshWallets() |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// SignHash calculates a ECDSA signature for the given hash. The produced
|
||||
// signature is in the [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { |
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock() |
||||
defer ks.mu.RUnlock() |
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address] |
||||
if !found { |
||||
return nil, ErrLocked |
||||
} |
||||
// Sign the hash using plain ECDSA operations
|
||||
return crypto.Sign(hash, unlockedKey.PrivateKey) |
||||
} |
||||
|
||||
// SignTx signs the given transaction with the requested account.
|
||||
func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock() |
||||
defer ks.mu.RUnlock() |
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address] |
||||
if !found { |
||||
return nil, ErrLocked |
||||
} |
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil { |
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey) |
||||
} |
||||
return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey) |
||||
} |
||||
|
||||
// SignHashWithPassphrase signs hash if the private key matching the given address
|
||||
// can be decrypted with the given passphrase. The produced signature is in the
|
||||
// [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { |
||||
_, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer zeroKey(key.PrivateKey) |
||||
return crypto.Sign(hash, key.PrivateKey) |
||||
} |
||||
|
||||
// SignTxWithPassphrase signs the transaction if the private key matching the
|
||||
// given address can be decrypted with the given passphrase.
|
||||
func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
_, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer zeroKey(key.PrivateKey) |
||||
|
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil { |
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey) |
||||
} |
||||
return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey) |
||||
} |
||||
|
||||
// Unlock unlocks the given account indefinitely.
|
||||
func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { |
||||
return ks.TimedUnlock(a, passphrase, 0) |
||||
} |
||||
|
||||
// Lock removes the private key with the given address from memory.
|
||||
func (ks *KeyStore) Lock(addr common.Address) error { |
||||
ks.mu.Lock() |
||||
if unl, found := ks.unlocked[addr]; found { |
||||
ks.mu.Unlock() |
||||
ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) |
||||
} else { |
||||
ks.mu.Unlock() |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// TimedUnlock unlocks the given account with the passphrase. The account
|
||||
// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
|
||||
// until the program exits. The account must match a unique key file.
|
||||
//
|
||||
// If the account address is already unlocked for a duration, TimedUnlock extends or
|
||||
// shortens the active unlock timeout. If the address was previously unlocked
|
||||
// indefinitely the timeout is not altered.
|
||||
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error { |
||||
a, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
ks.mu.Lock() |
||||
defer ks.mu.Unlock() |
||||
u, found := ks.unlocked[a.Address] |
||||
if found { |
||||
if u.abort == nil { |
||||
// The address was unlocked indefinitely, so unlocking
|
||||
// it with a timeout would be confusing.
|
||||
zeroKey(key.PrivateKey) |
||||
return nil |
||||
} |
||||
// Terminate the expire goroutine and replace it below.
|
||||
close(u.abort) |
||||
} |
||||
if timeout > 0 { |
||||
u = &unlocked{Key: key, abort: make(chan struct{})} |
||||
go ks.expire(a.Address, u, timeout) |
||||
} else { |
||||
u = &unlocked{Key: key} |
||||
} |
||||
ks.unlocked[a.Address] = u |
||||
return nil |
||||
} |
||||
|
||||
// Find resolves the given account into a unique entry in the keystore.
|
||||
func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { |
||||
ks.cache.maybeReload() |
||||
ks.cache.mu.Lock() |
||||
a, err := ks.cache.find(a) |
||||
ks.cache.mu.Unlock() |
||||
return a, err |
||||
} |
||||
|
||||
func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { |
||||
a, err := ks.Find(a) |
||||
if err != nil { |
||||
return a, nil, err |
||||
} |
||||
key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) |
||||
return a, key, err |
||||
} |
||||
|
||||
func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Duration) { |
||||
t := time.NewTimer(timeout) |
||||
defer t.Stop() |
||||
select { |
||||
case <-u.abort: |
||||
// just quit
|
||||
case <-t.C: |
||||
ks.mu.Lock() |
||||
// only drop if it's still the same key instance that dropLater
|
||||
// was launched with. we can check that using pointer equality
|
||||
// because the map stores a new pointer every time the key is
|
||||
// unlocked.
|
||||
if ks.unlocked[addr] == u { |
||||
zeroKey(u.PrivateKey) |
||||
delete(ks.unlocked, addr) |
||||
} |
||||
ks.mu.Unlock() |
||||
} |
||||
} |
||||
|
||||
// NewAccount generates a new key and stores it into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { |
||||
_, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) |
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
// Add the account to the cache immediately rather
|
||||
// than waiting for file system notifications to pick it up.
|
||||
ks.cache.add(account) |
||||
ks.refreshWallets() |
||||
return account, nil |
||||
} |
||||
|
||||
// Export exports as a JSON key, encrypted with newPassphrase.
|
||||
func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { |
||||
_, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var N, P int |
||||
if store, ok := ks.storage.(*keyStorePassphrase); ok { |
||||
N, P = store.scryptN, store.scryptP |
||||
} else { |
||||
N, P = StandardScryptN, StandardScryptP |
||||
} |
||||
return EncryptKey(key, newPassphrase, N, P) |
||||
} |
||||
|
||||
// Import stores the given encrypted JSON key into the key directory.
|
||||
func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { |
||||
key, err := DecryptKey(keyJSON, passphrase) |
||||
if key != nil && key.PrivateKey != nil { |
||||
defer zeroKey(key.PrivateKey) |
||||
} |
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
return ks.importKey(key, newPassphrase) |
||||
} |
||||
|
||||
// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
|
||||
func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { |
||||
key := newKeyFromECDSA(priv) |
||||
if ks.cache.hasAddress(key.Address) { |
||||
return accounts.Account{}, fmt.Errorf("account already exists") |
||||
} |
||||
return ks.importKey(key, passphrase) |
||||
} |
||||
|
||||
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { |
||||
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} |
||||
if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
ks.cache.add(a) |
||||
ks.refreshWallets() |
||||
return a, nil |
||||
} |
||||
|
||||
// Update changes the passphrase of an existing account.
|
||||
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { |
||||
a, key, err := ks.getDecryptedKey(a, passphrase) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) |
||||
} |
||||
|
||||
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
|
||||
// a key file in the key directory. The key file is encrypted with the same passphrase.
|
||||
func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { |
||||
a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) |
||||
if err != nil { |
||||
return a, err |
||||
} |
||||
ks.cache.add(a) |
||||
ks.refreshWallets() |
||||
return a, nil |
||||
} |
||||
|
||||
// zeroKey zeroes a private key in memory.
|
||||
func zeroKey(k *ecdsa.PrivateKey) { |
||||
b := k.D.Bits() |
||||
for i := range b { |
||||
b[i] = 0 |
||||
} |
||||
} |
@ -0,0 +1,387 @@ |
||||
// 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 keystore |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"math/rand" |
||||
"os" |
||||
"runtime" |
||||
"sort" |
||||
"strings" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
) |
||||
|
||||
var testSigData = make([]byte, 32) |
||||
|
||||
func TestKeyStore(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
a, err := ks.NewAccount("foo") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !strings.HasPrefix(a.URL.Path, dir) { |
||||
t.Errorf("account file %s doesn't have dir prefix", a.URL) |
||||
} |
||||
stat, err := os.Stat(a.URL.Path) |
||||
if err != nil { |
||||
t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) |
||||
} |
||||
if runtime.GOOS != "windows" && stat.Mode() != 0600 { |
||||
t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600) |
||||
} |
||||
if !ks.HasAddress(a.Address) { |
||||
t.Errorf("HasAccount(%x) should've returned true", a.Address) |
||||
} |
||||
if err := ks.Update(a, "foo", "bar"); err != nil { |
||||
t.Errorf("Update error: %v", err) |
||||
} |
||||
if err := ks.Delete(a, "bar"); err != nil { |
||||
t.Errorf("Delete error: %v", err) |
||||
} |
||||
if common.FileExist(a.URL.Path) { |
||||
t.Errorf("account file %s should be gone after Delete", a.URL) |
||||
} |
||||
if ks.HasAddress(a.Address) { |
||||
t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address) |
||||
} |
||||
} |
||||
|
||||
func TestSign(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "" // not used but required by API
|
||||
a1, err := ks.NewAccount(pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if err := ks.Unlock(a1, ""); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestSignWithPassphrase(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "passwd" |
||||
acc, err := ks.NewAccount(pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked { |
||||
t.Fatal("expected account to be locked") |
||||
} |
||||
|
||||
_, err = ks.SignHashWithPassphrase(acc, pass, testSigData) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked { |
||||
t.Fatal("expected account to be locked") |
||||
} |
||||
|
||||
if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil { |
||||
t.Fatal("expected SignHashWithPassphrase to fail with invalid password") |
||||
} |
||||
} |
||||
|
||||
func TestTimedUnlock(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "foo" |
||||
a1, err := ks.NewAccount(pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Signing without passphrase fails because account is locked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != ErrLocked { |
||||
t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) |
||||
} |
||||
|
||||
// Signing with passphrase works
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != nil { |
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err) |
||||
} |
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond) |
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != ErrLocked { |
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) |
||||
} |
||||
} |
||||
|
||||
func TestOverrideUnlock(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "foo" |
||||
a1, err := ks.NewAccount(pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Unlock indefinitely.
|
||||
if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != nil { |
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err) |
||||
} |
||||
|
||||
// reset unlock to a shorter period, invalidates the previous unlock
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Signing without passphrase still works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != nil { |
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err) |
||||
} |
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond) |
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) |
||||
if err != ErrLocked { |
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) |
||||
} |
||||
} |
||||
|
||||
// This test should fail under -race if signing races the expiration goroutine.
|
||||
func TestSignRace(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
// Create a test account.
|
||||
a1, err := ks.NewAccount("") |
||||
if err != nil { |
||||
t.Fatal("could not create the test account", err) |
||||
} |
||||
|
||||
if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil { |
||||
t.Fatal("could not unlock the test account", err) |
||||
} |
||||
end := time.Now().Add(500 * time.Millisecond) |
||||
for time.Now().Before(end) { |
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked { |
||||
return |
||||
} else if err != nil { |
||||
t.Errorf("Sign error: %v", err) |
||||
return |
||||
} |
||||
time.Sleep(1 * time.Millisecond) |
||||
} |
||||
t.Errorf("Account did not lock within the timeout") |
||||
} |
||||
|
||||
// Tests that the wallet notifier loop starts and stops correctly based on the
|
||||
// addition and removal of wallet event subscriptions.
|
||||
func TestWalletNotifierLifecycle(t *testing.T) { |
||||
// Create a temporary kesytore to test with
|
||||
dir, ks := tmpKeyStore(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
// Ensure that the notification updater is not running yet
|
||||
time.Sleep(250 * time.Millisecond) |
||||
ks.mu.RLock() |
||||
updating := ks.updating |
||||
ks.mu.RUnlock() |
||||
|
||||
if updating { |
||||
t.Errorf("wallet notifier running without subscribers") |
||||
} |
||||
// Subscribe to the wallet feed and ensure the updater boots up
|
||||
updates := make(chan accounts.WalletEvent) |
||||
|
||||
subs := make([]event.Subscription, 2) |
||||
for i := 0; i < len(subs); i++ { |
||||
// Create a new subscription
|
||||
subs[i] = ks.Subscribe(updates) |
||||
|
||||
// Ensure the notifier comes online
|
||||
time.Sleep(250 * time.Millisecond) |
||||
ks.mu.RLock() |
||||
updating = ks.updating |
||||
ks.mu.RUnlock() |
||||
|
||||
if !updating { |
||||
t.Errorf("sub %d: wallet notifier not running after subscription", i) |
||||
} |
||||
} |
||||
// Unsubscribe and ensure the updater terminates eventually
|
||||
for i := 0; i < len(subs); i++ { |
||||
// Close an existing subscription
|
||||
subs[i].Unsubscribe() |
||||
|
||||
// Ensure the notifier shuts down at and only at the last close
|
||||
for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ { |
||||
ks.mu.RLock() |
||||
updating = ks.updating |
||||
ks.mu.RUnlock() |
||||
|
||||
if i < len(subs)-1 && !updating { |
||||
t.Fatalf("sub %d: event notifier stopped prematurely", i) |
||||
} |
||||
if i == len(subs)-1 && !updating { |
||||
return |
||||
} |
||||
time.Sleep(250 * time.Millisecond) |
||||
} |
||||
} |
||||
t.Errorf("wallet notifier didn't terminate after unsubscribe") |
||||
} |
||||
|
||||
type walletEvent struct { |
||||
accounts.WalletEvent |
||||
a accounts.Account |
||||
} |
||||
|
||||
// Tests that wallet notifications and correctly fired when accounts are added
|
||||
// or deleted from the keystore.
|
||||
func TestWalletNotifications(t *testing.T) { |
||||
dir, ks := tmpKeyStore(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
// Subscribe to the wallet feed and collect events.
|
||||
var ( |
||||
events []walletEvent |
||||
updates = make(chan accounts.WalletEvent) |
||||
sub = ks.Subscribe(updates) |
||||
) |
||||
defer sub.Unsubscribe() |
||||
go func() { |
||||
for { |
||||
select { |
||||
case ev := <-updates: |
||||
events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]}) |
||||
case <-sub.Err(): |
||||
close(updates) |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
|
||||
// Randomly add and remove accounts.
|
||||
var ( |
||||
live = make(map[common.Address]accounts.Account) |
||||
wantEvents []walletEvent |
||||
) |
||||
for i := 0; i < 1024; i++ { |
||||
if create := len(live) == 0 || rand.Int()%4 > 0; create { |
||||
// Add a new account and ensure wallet notifications arrives
|
||||
account, err := ks.NewAccount("") |
||||
if err != nil { |
||||
t.Fatalf("failed to create test account: %v", err) |
||||
} |
||||
live[account.Address] = account |
||||
wantEvents = append(wantEvents, walletEvent{accounts.WalletEvent{Kind: accounts.WalletArrived}, account}) |
||||
} else { |
||||
// Delete a random account.
|
||||
var account accounts.Account |
||||
for _, a := range live { |
||||
account = a |
||||
break |
||||
} |
||||
if err := ks.Delete(account, ""); err != nil { |
||||
t.Fatalf("failed to delete test account: %v", err) |
||||
} |
||||
delete(live, account.Address) |
||||
wantEvents = append(wantEvents, walletEvent{accounts.WalletEvent{Kind: accounts.WalletDropped}, account}) |
||||
} |
||||
} |
||||
|
||||
// Shut down the event collector and check events.
|
||||
sub.Unsubscribe() |
||||
<-updates |
||||
checkAccounts(t, live, ks.Wallets()) |
||||
checkEvents(t, wantEvents, events) |
||||
} |
||||
|
||||
// checkAccounts checks that all known live accounts are present in the wallet list.
|
||||
func checkAccounts(t *testing.T, live map[common.Address]accounts.Account, wallets []accounts.Wallet) { |
||||
if len(live) != len(wallets) { |
||||
t.Errorf("wallet list doesn't match required accounts: have %d, want %d", len(wallets), len(live)) |
||||
return |
||||
} |
||||
liveList := make([]accounts.Account, 0, len(live)) |
||||
for _, account := range live { |
||||
liveList = append(liveList, account) |
||||
} |
||||
sort.Sort(accountsByURL(liveList)) |
||||
for j, wallet := range wallets { |
||||
if accs := wallet.Accounts(); len(accs) != 1 { |
||||
t.Errorf("wallet %d: contains invalid number of accounts: have %d, want 1", j, len(accs)) |
||||
} else if accs[0] != liveList[j] { |
||||
t.Errorf("wallet %d: account mismatch: have %v, want %v", j, accs[0], liveList[j]) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// checkEvents checks that all events in 'want' are present in 'have'. Events may be present multiple times.
|
||||
func checkEvents(t *testing.T, want []walletEvent, have []walletEvent) { |
||||
for _, wantEv := range want { |
||||
nmatch := 0 |
||||
for ; len(have) > 0; nmatch++ { |
||||
if have[0].Kind != wantEv.Kind || have[0].a != wantEv.a { |
||||
break |
||||
} |
||||
have = have[1:] |
||||
} |
||||
if nmatch == 0 { |
||||
t.Fatalf("can't find event with Kind=%v for %x", wantEv.Kind, wantEv.a.Address) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) { |
||||
d, err := ioutil.TempDir("", "eth-keystore-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
new := NewPlaintextKeyStore |
||||
if encrypted { |
||||
new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) } |
||||
} |
||||
return d, new(d) |
||||
} |
@ -0,0 +1,356 @@ |
||||
// Copyright 2014 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/>.
|
||||
|
||||
/* |
||||
|
||||
This key store behaves as KeyStorePlain with the difference that |
||||
the private key is encrypted and on disk uses another JSON encoding. |
||||
|
||||
The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
|
||||
|
||||
*/ |
||||
|
||||
package keystore |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/aes" |
||||
"crypto/rand" |
||||
"crypto/sha256" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/pborman/uuid" |
||||
"golang.org/x/crypto/pbkdf2" |
||||
"golang.org/x/crypto/scrypt" |
||||
) |
||||
|
||||
const ( |
||||
keyHeaderKDF = "scrypt" |
||||
|
||||
// StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptN = 1 << 18 |
||||
|
||||
// StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptP = 1 |
||||
|
||||
// LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptN = 1 << 12 |
||||
|
||||
// LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptP = 6 |
||||
|
||||
scryptR = 8 |
||||
scryptDKLen = 32 |
||||
) |
||||
|
||||
type keyStorePassphrase struct { |
||||
keysDirPath string |
||||
scryptN int |
||||
scryptP int |
||||
// skipKeyFileVerification disables the security-feature which does
|
||||
// reads and decrypts any newly created keyfiles. This should be 'false' in all
|
||||
// cases except tests -- setting this to 'true' is not recommended.
|
||||
skipKeyFileVerification bool |
||||
} |
||||
|
||||
func (ks keyStorePassphrase) GetKey(addr common.Address, filename, auth string) (*Key, error) { |
||||
// Load the key from the keystore and decrypt its contents
|
||||
keyjson, err := ioutil.ReadFile(filename) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
key, err := DecryptKey(keyjson, auth) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Make sure we're really operating on the requested key (no swap attacks)
|
||||
if key.Address != addr { |
||||
return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr) |
||||
} |
||||
return key, nil |
||||
} |
||||
|
||||
// StoreKey generates a key, encrypts with 'auth' and stores in the given directory
|
||||
func StoreKey(dir, auth string, scryptN, scryptP int) (accounts.Account, error) { |
||||
_, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP, false}, rand.Reader, auth) |
||||
return a, err |
||||
} |
||||
|
||||
func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { |
||||
keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// Write into temporary file
|
||||
tmpName, err := writeTemporaryKeyFile(filename, keyjson) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !ks.skipKeyFileVerification { |
||||
// Verify that we can decrypt the file with the given password.
|
||||
_, err = ks.GetKey(key.Address, tmpName, auth) |
||||
if err != nil { |
||||
msg := "An error was encountered when saving and verifying the keystore file. \n" + |
||||
"This indicates that the keystore is corrupted. \n" + |
||||
"The corrupted file is stored at \n%v\n" + |
||||
"Please file a ticket at:\n\n" + |
||||
"https://github.com/ethereum/go-ethereum/issues." + |
||||
"The error was : %s" |
||||
return fmt.Errorf(msg, tmpName, err) |
||||
} |
||||
} |
||||
return os.Rename(tmpName, filename) |
||||
} |
||||
|
||||
func (ks keyStorePassphrase) JoinPath(filename string) string { |
||||
if filepath.IsAbs(filename) { |
||||
return filename |
||||
} |
||||
return filepath.Join(ks.keysDirPath, filename) |
||||
} |
||||
|
||||
// Encryptdata encrypts the data given as 'data' with the password 'auth'.
|
||||
func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { |
||||
|
||||
salt := make([]byte, 32) |
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil { |
||||
panic("reading from crypto/rand failed: " + err.Error()) |
||||
} |
||||
derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) |
||||
if err != nil { |
||||
return CryptoJSON{}, err |
||||
} |
||||
encryptKey := derivedKey[:16] |
||||
|
||||
iv := make([]byte, aes.BlockSize) // 16
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil { |
||||
panic("reading from crypto/rand failed: " + err.Error()) |
||||
} |
||||
cipherText, err := aesCTRXOR(encryptKey, data, iv) |
||||
if err != nil { |
||||
return CryptoJSON{}, err |
||||
} |
||||
mac := crypto.Keccak256(derivedKey[16:32], cipherText) |
||||
|
||||
scryptParamsJSON := make(map[string]interface{}, 5) |
||||
scryptParamsJSON["n"] = scryptN |
||||
scryptParamsJSON["r"] = scryptR |
||||
scryptParamsJSON["p"] = scryptP |
||||
scryptParamsJSON["dklen"] = scryptDKLen |
||||
scryptParamsJSON["salt"] = hex.EncodeToString(salt) |
||||
cipherParamsJSON := cipherparamsJSON{ |
||||
IV: hex.EncodeToString(iv), |
||||
} |
||||
|
||||
cryptoStruct := CryptoJSON{ |
||||
Cipher: "aes-128-ctr", |
||||
CipherText: hex.EncodeToString(cipherText), |
||||
CipherParams: cipherParamsJSON, |
||||
KDF: keyHeaderKDF, |
||||
KDFParams: scryptParamsJSON, |
||||
MAC: hex.EncodeToString(mac), |
||||
} |
||||
return cryptoStruct, nil |
||||
} |
||||
|
||||
// EncryptKey encrypts a key using the specified scrypt parameters into a json
|
||||
// blob that can be decrypted later on.
|
||||
func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { |
||||
keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) |
||||
cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
encryptedKeyJSONV3 := encryptedKeyJSONV3{ |
||||
hex.EncodeToString(key.Address[:]), |
||||
cryptoStruct, |
||||
key.Id.String(), |
||||
version, |
||||
} |
||||
return json.Marshal(encryptedKeyJSONV3) |
||||
} |
||||
|
||||
// DecryptKey decrypts a key from a json blob, returning the private key itself.
|
||||
func DecryptKey(keyjson []byte, auth string) (*Key, error) { |
||||
// Parse the json into a simple map to fetch the key version
|
||||
m := make(map[string]interface{}) |
||||
if err := json.Unmarshal(keyjson, &m); err != nil { |
||||
return nil, err |
||||
} |
||||
// Depending on the version try to parse one way or another
|
||||
var ( |
||||
keyBytes, keyId []byte |
||||
err error |
||||
) |
||||
if version, ok := m["version"].(string); ok && version == "1" { |
||||
k := new(encryptedKeyJSONV1) |
||||
if err := json.Unmarshal(keyjson, k); err != nil { |
||||
return nil, err |
||||
} |
||||
keyBytes, keyId, err = decryptKeyV1(k, auth) |
||||
} else { |
||||
k := new(encryptedKeyJSONV3) |
||||
if err := json.Unmarshal(keyjson, k); err != nil { |
||||
return nil, err |
||||
} |
||||
keyBytes, keyId, err = decryptKeyV3(k, auth) |
||||
} |
||||
// Handle any decryption errors and return the key
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
key := crypto.ToECDSAUnsafe(keyBytes) |
||||
|
||||
return &Key{ |
||||
Id: uuid.UUID(keyId), |
||||
Address: crypto.PubkeyToAddress(key.PublicKey), |
||||
PrivateKey: key, |
||||
}, nil |
||||
} |
||||
|
||||
func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { |
||||
if cryptoJson.Cipher != "aes-128-ctr" { |
||||
return nil, fmt.Errorf("Cipher not supported: %v", cryptoJson.Cipher) |
||||
} |
||||
mac, err := hex.DecodeString(cryptoJson.MAC) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
cipherText, err := hex.DecodeString(cryptoJson.CipherText) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
derivedKey, err := getKDFKey(cryptoJson, auth) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) |
||||
if !bytes.Equal(calculatedMAC, mac) { |
||||
return nil, ErrDecrypt |
||||
} |
||||
|
||||
plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return plainText, err |
||||
} |
||||
|
||||
func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { |
||||
if keyProtected.Version != version { |
||||
return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) |
||||
} |
||||
keyId = uuid.Parse(keyProtected.Id) |
||||
plainText, err := DecryptDataV3(keyProtected.Crypto, auth) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return plainText, keyId, err |
||||
} |
||||
|
||||
func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { |
||||
keyId = uuid.Parse(keyProtected.Id) |
||||
mac, err := hex.DecodeString(keyProtected.Crypto.MAC) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
derivedKey, err := getKDFKey(keyProtected.Crypto, auth) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) |
||||
if !bytes.Equal(calculatedMAC, mac) { |
||||
return nil, nil, ErrDecrypt |
||||
} |
||||
|
||||
plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return plainText, keyId, err |
||||
} |
||||
|
||||
func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { |
||||
authArray := []byte(auth) |
||||
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) |
||||
|
||||
if cryptoJSON.KDF == keyHeaderKDF { |
||||
n := ensureInt(cryptoJSON.KDFParams["n"]) |
||||
r := ensureInt(cryptoJSON.KDFParams["r"]) |
||||
p := ensureInt(cryptoJSON.KDFParams["p"]) |
||||
return scrypt.Key(authArray, salt, n, r, p, dkLen) |
||||
|
||||
} else if cryptoJSON.KDF == "pbkdf2" { |
||||
c := ensureInt(cryptoJSON.KDFParams["c"]) |
||||
prf := cryptoJSON.KDFParams["prf"].(string) |
||||
if prf != "hmac-sha256" { |
||||
return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf) |
||||
} |
||||
key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) |
||||
return key, nil |
||||
} |
||||
|
||||
return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) |
||||
} |
||||
|
||||
// TODO: can we do without this when unmarshalling dynamic JSON?
|
||||
// why do integers in KDF params end up as float64 and not int after
|
||||
// unmarshal?
|
||||
func ensureInt(x interface{}) int { |
||||
res, ok := x.(int) |
||||
if !ok { |
||||
res = int(x.(float64)) |
||||
} |
||||
return res |
||||
} |
@ -0,0 +1,60 @@ |
||||
// Copyright 2016 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 keystore |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
const ( |
||||
veryLightScryptN = 2 |
||||
veryLightScryptP = 1 |
||||
) |
||||
|
||||
// Tests that a json key file can be decrypted and encrypted in multiple rounds.
|
||||
func TestKeyEncryptDecrypt(t *testing.T) { |
||||
keyjson, err := ioutil.ReadFile("testdata/very-light-scrypt.json") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
password := "" |
||||
address := common.HexToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") |
||||
|
||||
// Do a few rounds of decryption and encryption
|
||||
for i := 0; i < 3; i++ { |
||||
// Try a bad password first
|
||||
if _, err := DecryptKey(keyjson, password+"bad"); err == nil { |
||||
t.Errorf("test %d: json key decrypted with bad password", i) |
||||
} |
||||
// Decrypt with the correct password
|
||||
key, err := DecryptKey(keyjson, password) |
||||
if err != nil { |
||||
t.Fatalf("test %d: json key failed to decrypt: %v", i, err) |
||||
} |
||||
if key.Address != address { |
||||
t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) |
||||
} |
||||
// Recrypt with a new password and start over
|
||||
password += "new data appended" |
||||
if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { |
||||
t.Errorf("test %d: failed to recrypt key %v", i, err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
// Copyright 2015 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 keystore |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
type keyStorePlain struct { |
||||
keysDirPath string |
||||
} |
||||
|
||||
func (ks keyStorePlain) GetKey(addr common.Address, filename, auth string) (*Key, error) { |
||||
fd, err := os.Open(filename) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer fd.Close() |
||||
key := new(Key) |
||||
if err := json.NewDecoder(fd).Decode(key); err != nil { |
||||
return nil, err |
||||
} |
||||
if key.Address != addr { |
||||
return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) |
||||
} |
||||
return key, nil |
||||
} |
||||
|
||||
func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { |
||||
content, err := json.Marshal(key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return writeKeyFile(filename, content) |
||||
} |
||||
|
||||
func (ks keyStorePlain) JoinPath(filename string) string { |
||||
if filepath.IsAbs(filename) { |
||||
return filename |
||||
} |
||||
return filepath.Join(ks.keysDirPath, filename) |
||||
} |
@ -0,0 +1,266 @@ |
||||
// Copyright 2014 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 keystore |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"encoding/hex" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { |
||||
d, err := ioutil.TempDir("", "geth-keystore-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if encrypted { |
||||
ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} |
||||
} else { |
||||
ks = &keyStorePlain{d} |
||||
} |
||||
return d, ks |
||||
} |
||||
|
||||
func TestKeyStorePlain(t *testing.T) { |
||||
dir, ks := tmpKeyStoreIface(t, false) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "" // not used but required by API
|
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !reflect.DeepEqual(k1.Address, k2.Address) { |
||||
t.Fatal(err) |
||||
} |
||||
if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestKeyStorePassphrase(t *testing.T) { |
||||
dir, ks := tmpKeyStoreIface(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "foo" |
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !reflect.DeepEqual(k1.Address, k2.Address) { |
||||
t.Fatal(err) |
||||
} |
||||
if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestKeyStorePassphraseDecryptionFail(t *testing.T) { |
||||
dir, ks := tmpKeyStoreIface(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
pass := "foo" |
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt { |
||||
t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) |
||||
} |
||||
} |
||||
|
||||
func TestImportPreSaleKey(t *testing.T) { |
||||
dir, ks := tmpKeyStoreIface(t, true) |
||||
defer os.RemoveAll(dir) |
||||
|
||||
// file content of a presale key file generated with:
|
||||
// python pyethsaletool.py genwallet
|
||||
// with password "foo"
|
||||
fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" |
||||
pass := "foo" |
||||
account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { |
||||
t.Errorf("imported account has wrong address %x", account.Address) |
||||
} |
||||
if !strings.HasPrefix(account.URL.Path, dir) { |
||||
t.Errorf("imported account file not in keystore directory: %q", account.URL) |
||||
} |
||||
} |
||||
|
||||
// Test and utils for the key store tests in the Ethereum JSON tests;
|
||||
// testdataKeyStoreTests/basic_tests.json
|
||||
type KeyStoreTestV3 struct { |
||||
Json encryptedKeyJSONV3 |
||||
Password string |
||||
Priv string |
||||
} |
||||
|
||||
type KeyStoreTestV1 struct { |
||||
Json encryptedKeyJSONV1 |
||||
Password string |
||||
Priv string |
||||
} |
||||
|
||||
func TestV3_PBKDF2_1(t *testing.T) { |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) |
||||
testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) |
||||
} |
||||
|
||||
var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") |
||||
|
||||
func skipIfSubmoduleMissing(t *testing.T) { |
||||
if !common.FileExist(testsSubmodule) { |
||||
t.Skipf("can't find JSON tests from submodule at %s", testsSubmodule) |
||||
} |
||||
} |
||||
|
||||
func TestV3_PBKDF2_2(t *testing.T) { |
||||
skipIfSubmoduleMissing(t) |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) |
||||
testDecryptV3(tests["test1"], t) |
||||
} |
||||
|
||||
func TestV3_PBKDF2_3(t *testing.T) { |
||||
skipIfSubmoduleMissing(t) |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) |
||||
testDecryptV3(tests["python_generated_test_with_odd_iv"], t) |
||||
} |
||||
|
||||
func TestV3_PBKDF2_4(t *testing.T) { |
||||
skipIfSubmoduleMissing(t) |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) |
||||
testDecryptV3(tests["evilnonce"], t) |
||||
} |
||||
|
||||
func TestV3_Scrypt_1(t *testing.T) { |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) |
||||
testDecryptV3(tests["wikipage_test_vector_scrypt"], t) |
||||
} |
||||
|
||||
func TestV3_Scrypt_2(t *testing.T) { |
||||
skipIfSubmoduleMissing(t) |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) |
||||
testDecryptV3(tests["test2"], t) |
||||
} |
||||
|
||||
func TestV1_1(t *testing.T) { |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) |
||||
testDecryptV1(tests["test1"], t) |
||||
} |
||||
|
||||
func TestV1_2(t *testing.T) { |
||||
t.Parallel() |
||||
ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} |
||||
addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") |
||||
file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" |
||||
k, err := ks.GetKey(addr, file, "g") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
privHex := hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)) |
||||
expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" |
||||
if privHex != expectedHex { |
||||
t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) |
||||
} |
||||
} |
||||
|
||||
func testDecryptV3(test KeyStoreTestV3, t *testing.T) { |
||||
privBytes, _, err := decryptKeyV3(&test.Json, test.Password) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
privHex := hex.EncodeToString(privBytes) |
||||
if test.Priv != privHex { |
||||
t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) |
||||
} |
||||
} |
||||
|
||||
func testDecryptV1(test KeyStoreTestV1, t *testing.T) { |
||||
privBytes, _, err := decryptKeyV1(&test.Json, test.Password) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
privHex := hex.EncodeToString(privBytes) |
||||
if test.Priv != privHex { |
||||
t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) |
||||
} |
||||
} |
||||
|
||||
func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { |
||||
tests := make(map[string]KeyStoreTestV3) |
||||
err := common.LoadJSON(file, &tests) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return tests |
||||
} |
||||
|
||||
func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { |
||||
tests := make(map[string]KeyStoreTestV1) |
||||
err := common.LoadJSON(file, &tests) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return tests |
||||
} |
||||
|
||||
func TestKeyForDirectICAP(t *testing.T) { |
||||
t.Parallel() |
||||
key := NewKeyForDirectICAP(rand.Reader) |
||||
if !strings.HasPrefix(key.Address.Hex(), "0x00") { |
||||
t.Errorf("Expected first address byte to be zero, have: %s", key.Address.Hex()) |
||||
} |
||||
} |
||||
|
||||
func TestV3_31_Byte_Key(t *testing.T) { |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) |
||||
testDecryptV3(tests["31_byte_key"], t) |
||||
} |
||||
|
||||
func TestV3_30_Byte_Key(t *testing.T) { |
||||
t.Parallel() |
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) |
||||
testDecryptV3(tests["30_byte_key"], t) |
||||
} |
@ -0,0 +1,147 @@ |
||||
// Copyright 2016 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 keystore |
||||
|
||||
import ( |
||||
"crypto/aes" |
||||
"crypto/cipher" |
||||
"crypto/sha256" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/pborman/uuid" |
||||
"golang.org/x/crypto/pbkdf2" |
||||
) |
||||
|
||||
// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON
|
||||
func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) { |
||||
key, err := decryptPreSaleKey(keyJSON, password) |
||||
if err != nil { |
||||
return accounts.Account{}, nil, err |
||||
} |
||||
key.Id = uuid.NewRandom() |
||||
a := accounts.Account{ |
||||
Address: key.Address, |
||||
URL: accounts.URL{ |
||||
Scheme: KeyStoreScheme, |
||||
Path: keyStore.JoinPath(keyFileName(key.Address)), |
||||
}, |
||||
} |
||||
err = keyStore.StoreKey(a.URL.Path, key, password) |
||||
return a, key, err |
||||
} |
||||
|
||||
func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { |
||||
preSaleKeyStruct := struct { |
||||
EncSeed string |
||||
EthAddr string |
||||
Email string |
||||
BtcAddr string |
||||
}{} |
||||
err = json.Unmarshal(fileContent, &preSaleKeyStruct) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) |
||||
if err != nil { |
||||
return nil, errors.New("invalid hex in encSeed") |
||||
} |
||||
if len(encSeedBytes) < 16 { |
||||
return nil, errors.New("invalid encSeed, too short") |
||||
} |
||||
iv := encSeedBytes[:16] |
||||
cipherText := encSeedBytes[16:] |
||||
/* |
||||
See https://github.com/ethereum/pyethsaletool
|
||||
|
||||
pyethsaletool generates the encryption key from password by |
||||
2000 rounds of PBKDF2 with HMAC-SHA-256 using password as salt (:(). |
||||
16 byte key length within PBKDF2 and resulting key is used as AES key |
||||
*/ |
||||
passBytes := []byte(password) |
||||
derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New) |
||||
plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ethPriv := crypto.Keccak256(plainText) |
||||
ecKey := crypto.ToECDSAUnsafe(ethPriv) |
||||
|
||||
key = &Key{ |
||||
Id: nil, |
||||
Address: crypto.PubkeyToAddress(ecKey.PublicKey), |
||||
PrivateKey: ecKey, |
||||
} |
||||
derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x"
|
||||
expectedAddr := preSaleKeyStruct.EthAddr |
||||
if derivedAddr != expectedAddr { |
||||
err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) |
||||
} |
||||
return key, err |
||||
} |
||||
|
||||
func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { |
||||
// AES-128 is selected due to size of encryptKey.
|
||||
aesBlock, err := aes.NewCipher(key) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
stream := cipher.NewCTR(aesBlock, iv) |
||||
outText := make([]byte, len(inText)) |
||||
stream.XORKeyStream(outText, inText) |
||||
return outText, err |
||||
} |
||||
|
||||
func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { |
||||
aesBlock, err := aes.NewCipher(key) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
decrypter := cipher.NewCBCDecrypter(aesBlock, iv) |
||||
paddedPlaintext := make([]byte, len(cipherText)) |
||||
decrypter.CryptBlocks(paddedPlaintext, cipherText) |
||||
plaintext := pkcs7Unpad(paddedPlaintext) |
||||
if plaintext == nil { |
||||
return nil, ErrDecrypt |
||||
} |
||||
return plaintext, err |
||||
} |
||||
|
||||
// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes
|
||||
func pkcs7Unpad(in []byte) []byte { |
||||
if len(in) == 0 { |
||||
return nil |
||||
} |
||||
|
||||
padding := in[len(in)-1] |
||||
if int(padding) > len(in) || padding > aes.BlockSize { |
||||
return nil |
||||
} else if padding == 0 { |
||||
return nil |
||||
} |
||||
|
||||
for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { |
||||
if in[i] != padding { |
||||
return nil |
||||
} |
||||
} |
||||
return in[:len(in)-int(padding)] |
||||
} |
@ -0,0 +1 @@ |
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1,21 @@ |
||||
This directory contains accounts for testing. |
||||
The passphrase that unlocks them is "foobar". |
||||
|
||||
The "good" key files which are supposed to be loadable are: |
||||
|
||||
- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 |
||||
Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 |
||||
- File: aaa |
||||
Address: 0xf466859ead1932d743d622cb74fc058882e8648a |
||||
- File: zzz |
||||
Address: 0x289d485d9771714cce91d3393d764e1311907acc |
||||
|
||||
The other files (including this README) are broken in various ways |
||||
and should not be picked up by package accounts: |
||||
|
||||
- File: no-address (missing address field, otherwise same as "aaa") |
||||
- File: garbage (file with random data) |
||||
- File: empty (file with no content) |
||||
- File: swapfile~ (should be skipped) |
||||
- File: .hiddenfile (should be skipped) |
||||
- File: foo/... (should be skipped because it is a directory) |
@ -0,0 +1 @@ |
||||
{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3} |
Binary file not shown.
@ -0,0 +1 @@ |
||||
{"crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"0000000000000000000000000000000000000000","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"289d485d9771714cce91d3393d764e1311907acc","crypto":{"cipher":"aes-128-ctr","ciphertext":"faf32ca89d286b107f5e6d842802e05263c49b78d46eac74e6109e9a963378ab","cipherparams":{"iv":"558833eec4a665a8c55608d7d503407d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"d571fff447ffb24314f9513f5160246f09997b857ac71348b73e785aab40dc04"},"mac":"21edb85ff7d0dab1767b9bf498f2c3cb7be7609490756bd32300bb213b59effe"},"id":"3279afcf-55ba-43ff-8997-02dcc46a6525","version":3} |
@ -0,0 +1 @@ |
||||
{"address":"cb61d5a9c4896fb9658090b597ef0e7be6f7b67e","Crypto":{"cipher":"aes-128-cbc","ciphertext":"6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0","cipherparams":{"iv":"35337770fc2117994ecdcad026bccff4"},"kdf":"scrypt","kdfparams":{"n":262144,"r":8,"p":1,"dklen":32,"salt":"9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f"},"mac":"3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644","version":"1"},"id":"e25f7c1f-d318-4f29-b62c-687190d4d299","version":"1"} |
@ -0,0 +1,28 @@ |
||||
{ |
||||
"test1": { |
||||
"json": { |
||||
"Crypto": { |
||||
"cipher": "aes-128-cbc", |
||||
"cipherparams": { |
||||
"iv": "35337770fc2117994ecdcad026bccff4" |
||||
}, |
||||
"ciphertext": "6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0", |
||||
"kdf": "scrypt", |
||||
"kdfparams": { |
||||
"dklen": 32, |
||||
"n": 262144, |
||||
"p": 1, |
||||
"r": 8, |
||||
"salt": "9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f" |
||||
}, |
||||
"mac": "3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644", |
||||
"version": "1" |
||||
}, |
||||
"address": "cb61d5a9c4896fb9658090b597ef0e7be6f7b67e", |
||||
"id": "e25f7c1f-d318-4f29-b62c-687190d4d299", |
||||
"version": "1" |
||||
}, |
||||
"password": "g", |
||||
"priv": "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" |
||||
} |
||||
} |
@ -0,0 +1,97 @@ |
||||
{ |
||||
"wikipage_test_vector_scrypt": { |
||||
"json": { |
||||
"crypto" : { |
||||
"cipher" : "aes-128-ctr", |
||||
"cipherparams" : { |
||||
"iv" : "83dbcc02d8ccb40e466191a123791e0e" |
||||
}, |
||||
"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", |
||||
"kdf" : "scrypt", |
||||
"kdfparams" : { |
||||
"dklen" : 32, |
||||
"n" : 262144, |
||||
"r" : 1, |
||||
"p" : 8, |
||||
"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" |
||||
}, |
||||
"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" |
||||
}, |
||||
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", |
||||
"version" : 3 |
||||
}, |
||||
"password": "testpassword", |
||||
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" |
||||
}, |
||||
"wikipage_test_vector_pbkdf2": { |
||||
"json": { |
||||
"crypto" : { |
||||
"cipher" : "aes-128-ctr", |
||||
"cipherparams" : { |
||||
"iv" : "6087dab2f9fdbbfaddc31a909735c1e6" |
||||
}, |
||||
"ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", |
||||
"kdf" : "pbkdf2", |
||||
"kdfparams" : { |
||||
"c" : 262144, |
||||
"dklen" : 32, |
||||
"prf" : "hmac-sha256", |
||||
"salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" |
||||
}, |
||||
"mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" |
||||
}, |
||||
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", |
||||
"version" : 3 |
||||
}, |
||||
"password": "testpassword", |
||||
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" |
||||
}, |
||||
"31_byte_key": { |
||||
"json": { |
||||
"crypto" : { |
||||
"cipher" : "aes-128-ctr", |
||||
"cipherparams" : { |
||||
"iv" : "e0c41130a323adc1446fc82f724bca2f" |
||||
}, |
||||
"ciphertext" : "9517cd5bdbe69076f9bf5057248c6c050141e970efa36ce53692d5d59a3984", |
||||
"kdf" : "scrypt", |
||||
"kdfparams" : { |
||||
"dklen" : 32, |
||||
"n" : 2, |
||||
"r" : 8, |
||||
"p" : 1, |
||||
"salt" : "711f816911c92d649fb4c84b047915679933555030b3552c1212609b38208c63" |
||||
}, |
||||
"mac" : "d5e116151c6aa71470e67a7d42c9620c75c4d23229847dcc127794f0732b0db5" |
||||
}, |
||||
"id" : "fecfc4ce-e956-48fd-953b-30f8b52ed66c", |
||||
"version" : 3 |
||||
}, |
||||
"password": "foo", |
||||
"priv": "fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35" |
||||
}, |
||||
"30_byte_key": { |
||||
"json": { |
||||
"crypto" : { |
||||
"cipher" : "aes-128-ctr", |
||||
"cipherparams" : { |
||||
"iv" : "3ca92af36ad7c2cd92454c59cea5ef00" |
||||
}, |
||||
"ciphertext" : "108b7d34f3442fc26ab1ab90ca91476ba6bfa8c00975a49ef9051dc675aa", |
||||
"kdf" : "scrypt", |
||||
"kdfparams" : { |
||||
"dklen" : 32, |
||||
"n" : 2, |
||||
"r" : 8, |
||||
"p" : 1, |
||||
"salt" : "d0769e608fb86cda848065642a9c6fa046845c928175662b8e356c77f914cd3b" |
||||
}, |
||||
"mac" : "75d0e6759f7b3cefa319c3be41680ab6beea7d8328653474bd06706d4cc67420" |
||||
}, |
||||
"id" : "a37e1559-5955-450d-8075-7b8931b392b2", |
||||
"version" : 3 |
||||
}, |
||||
"password": "foo", |
||||
"priv": "81c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018" |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
{"address":"45dea0fb0bba44f4fcf290bba71fd57d7117cbb8","crypto":{"cipher":"aes-128-ctr","ciphertext":"b87781948a1befd247bff51ef4063f716cf6c2d3481163e9a8f42e1f9bb74145","cipherparams":{"iv":"dc4926b48a105133d2f16b96833abf1e"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":2,"p":1,"r":8,"salt":"004244bbdc51cadda545b1cfa43cff9ed2ae88e08c61f1479dbb45410722f8f0"},"mac":"39990c1684557447940d4c69e06b1b82b2aceacb43f284df65c956daf3046b85"},"id":"ce541d8d-c79b-40f8-9f8c-20f59616faba","version":3} |
@ -0,0 +1,148 @@ |
||||
// 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 keystore |
||||
|
||||
import ( |
||||
"math/big" |
||||
|
||||
ethereum "github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// keystoreWallet implements the accounts.Wallet interface for the original
|
||||
// keystore.
|
||||
type keystoreWallet struct { |
||||
account accounts.Account // Single account contained in this wallet
|
||||
keystore *KeyStore // Keystore where the account originates from
|
||||
} |
||||
|
||||
// URL implements accounts.Wallet, returning the URL of the account within.
|
||||
func (w *keystoreWallet) URL() accounts.URL { |
||||
return w.account.URL |
||||
} |
||||
|
||||
// Status implements accounts.Wallet, returning whether the account held by the
|
||||
// keystore wallet is unlocked or not.
|
||||
func (w *keystoreWallet) Status() (string, error) { |
||||
w.keystore.mu.RLock() |
||||
defer w.keystore.mu.RUnlock() |
||||
|
||||
if _, ok := w.keystore.unlocked[w.account.Address]; ok { |
||||
return "Unlocked", nil |
||||
} |
||||
return "Locked", nil |
||||
} |
||||
|
||||
// Open implements accounts.Wallet, but is a noop for plain wallets since there
|
||||
// is no connection or decryption step necessary to access the list of accounts.
|
||||
func (w *keystoreWallet) Open(passphrase string) error { return nil } |
||||
|
||||
// Close implements accounts.Wallet, but is a noop for plain wallets since there
|
||||
// is no meaningful open operation.
|
||||
func (w *keystoreWallet) Close() error { return nil } |
||||
|
||||
// Accounts implements accounts.Wallet, returning an account list consisting of
|
||||
// a single account that the plain kestore wallet contains.
|
||||
func (w *keystoreWallet) Accounts() []accounts.Account { |
||||
return []accounts.Account{w.account} |
||||
} |
||||
|
||||
// Contains implements accounts.Wallet, returning whether a particular account is
|
||||
// or is not wrapped by this wallet instance.
|
||||
func (w *keystoreWallet) Contains(account accounts.Account) bool { |
||||
return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL) |
||||
} |
||||
|
||||
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
|
||||
// is no notion of hierarchical account derivation for plain keystore accounts.
|
||||
func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { |
||||
return accounts.Account{}, accounts.ErrNotSupported |
||||
} |
||||
|
||||
// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
|
||||
// there is no notion of hierarchical account derivation for plain keystore accounts.
|
||||
func (w *keystoreWallet) SelfDerive(bases []accounts.DerivationPath, chain ethereum.ChainStateReader) { |
||||
} |
||||
|
||||
// signHash attempts to sign the given hash with
|
||||
// the given account. If the wallet does not wrap this particular account, an
|
||||
// error is returned to avoid account leakage (even though in theory we may be
|
||||
// able to sign via our shared keystore backend).
|
||||
func (w *keystoreWallet) signHash(account accounts.Account, hash []byte) ([]byte, error) { |
||||
// Make sure the requested account is contained within
|
||||
if !w.Contains(account) { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignHash(account, hash) |
||||
} |
||||
|
||||
// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
|
||||
func (w *keystoreWallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { |
||||
return w.signHash(account, crypto.Keccak256(data)) |
||||
} |
||||
|
||||
// SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed
|
||||
func (w *keystoreWallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { |
||||
// Make sure the requested account is contained within
|
||||
if !w.Contains(account) { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data)) |
||||
} |
||||
|
||||
func (w *keystoreWallet) SignText(account accounts.Account, text []byte) ([]byte, error) { |
||||
return w.signHash(account, accounts.TextHash(text)) |
||||
} |
||||
|
||||
// SignTextWithPassphrase implements accounts.Wallet, attempting to sign the
|
||||
// given hash with the given account using passphrase as extra authentication.
|
||||
func (w *keystoreWallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { |
||||
// Make sure the requested account is contained within
|
||||
if !w.Contains(account) { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) |
||||
} |
||||
|
||||
// SignTx implements accounts.Wallet, attempting to sign the given transaction
|
||||
// with the given account. If the wallet does not wrap this particular account,
|
||||
// an error is returned to avoid account leakage (even though in theory we may
|
||||
// be able to sign via our shared keystore backend).
|
||||
func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
// Make sure the requested account is contained within
|
||||
if !w.Contains(account) { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignTx(account, tx, chainID) |
||||
} |
||||
|
||||
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||
// transaction with the given account using passphrase as extra authentication.
|
||||
func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
// Make sure the requested account is contained within
|
||||
if !w.Contains(account) { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID) |
||||
} |
@ -0,0 +1,108 @@ |
||||
// Copyright 2016 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/>.
|
||||
|
||||
// +build darwin,!ios freebsd linux,!arm64 netbsd solaris
|
||||
|
||||
package keystore |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/rjeczalik/notify" |
||||
) |
||||
|
||||
type watcher struct { |
||||
ac *accountCache |
||||
starting bool |
||||
running bool |
||||
ev chan notify.EventInfo |
||||
quit chan struct{} |
||||
} |
||||
|
||||
func newWatcher(ac *accountCache) *watcher { |
||||
return &watcher{ |
||||
ac: ac, |
||||
ev: make(chan notify.EventInfo, 10), |
||||
quit: make(chan struct{}), |
||||
} |
||||
} |
||||
|
||||
// starts the watcher loop in the background.
|
||||
// Start a watcher in the background if that's not already in progress.
|
||||
// The caller must hold w.ac.mu.
|
||||
func (w *watcher) start() { |
||||
if w.starting || w.running { |
||||
return |
||||
} |
||||
w.starting = true |
||||
go w.loop() |
||||
} |
||||
|
||||
func (w *watcher) close() { |
||||
close(w.quit) |
||||
} |
||||
|
||||
func (w *watcher) loop() { |
||||
defer func() { |
||||
w.ac.mu.Lock() |
||||
w.running = false |
||||
w.starting = false |
||||
w.ac.mu.Unlock() |
||||
}() |
||||
logger := log.New("path", w.ac.keydir) |
||||
|
||||
if err := notify.Watch(w.ac.keydir, w.ev, notify.All); err != nil { |
||||
logger.Trace("Failed to watch keystore folder", "err", err) |
||||
return |
||||
} |
||||
defer notify.Stop(w.ev) |
||||
logger.Trace("Started watching keystore folder") |
||||
defer logger.Trace("Stopped watching keystore folder") |
||||
|
||||
w.ac.mu.Lock() |
||||
w.running = true |
||||
w.ac.mu.Unlock() |
||||
|
||||
// Wait for file system events and reload.
|
||||
// When an event occurs, the reload call is delayed a bit so that
|
||||
// multiple events arriving quickly only cause a single reload.
|
||||
var ( |
||||
debounceDuration = 500 * time.Millisecond |
||||
rescanTriggered = false |
||||
debounce = time.NewTimer(0) |
||||
) |
||||
// Ignore initial trigger
|
||||
if !debounce.Stop() { |
||||
<-debounce.C |
||||
} |
||||
defer debounce.Stop() |
||||
for { |
||||
select { |
||||
case <-w.quit: |
||||
return |
||||
case <-w.ev: |
||||
// Trigger the scan (with delay), if not already triggered
|
||||
if !rescanTriggered { |
||||
debounce.Reset(debounceDuration) |
||||
rescanTriggered = true |
||||
} |
||||
case <-debounce.C: |
||||
w.ac.scanAccounts() |
||||
rescanTriggered = false |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
// Copyright 2016 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/>.
|
||||
|
||||
// +build ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris
|
||||
|
||||
// This is the fallback implementation of directory watching.
|
||||
// It is used on unsupported platforms.
|
||||
|
||||
package keystore |
||||
|
||||
type watcher struct{ running bool } |
||||
|
||||
func newWatcher(*accountCache) *watcher { return new(watcher) } |
||||
func (*watcher) start() {} |
||||
func (*watcher) close() {} |
@ -0,0 +1,214 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"reflect" |
||||
"sort" |
||||
"sync" |
||||
|
||||
"github.com/ethereum/go-ethereum/event" |
||||
) |
||||
|
||||
// Config contains the settings of the global account manager.
|
||||
//
|
||||
// TODO(rjl493456442, karalabe, holiman): Get rid of this when account management
|
||||
// is removed in favor of Clef.
|
||||
type Config struct { |
||||
InsecureUnlockAllowed bool // Whether account unlocking in insecure environment is allowed
|
||||
} |
||||
|
||||
// Manager is an overarching account manager that can communicate with various
|
||||
// backends for signing transactions.
|
||||
type Manager struct { |
||||
config *Config // Global account manager configurations
|
||||
backends map[reflect.Type][]Backend // Index of backends currently registered
|
||||
updaters []event.Subscription // Wallet update subscriptions for all backends
|
||||
updates chan WalletEvent // Subscription sink for backend wallet changes
|
||||
wallets []Wallet // Cache of all wallets from all registered backends
|
||||
|
||||
feed event.Feed // Wallet feed notifying of arrivals/departures
|
||||
|
||||
quit chan chan error |
||||
lock sync.RWMutex |
||||
} |
||||
|
||||
// NewManager creates a generic account manager to sign transaction via various
|
||||
// supported backends.
|
||||
func NewManager(config *Config, backends ...Backend) *Manager { |
||||
// Retrieve the initial list of wallets from the backends and sort by URL
|
||||
var wallets []Wallet |
||||
for _, backend := range backends { |
||||
wallets = merge(wallets, backend.Wallets()...) |
||||
} |
||||
// Subscribe to wallet notifications from all backends
|
||||
updates := make(chan WalletEvent, 4*len(backends)) |
||||
|
||||
subs := make([]event.Subscription, len(backends)) |
||||
for i, backend := range backends { |
||||
subs[i] = backend.Subscribe(updates) |
||||
} |
||||
// Assemble the account manager and return
|
||||
am := &Manager{ |
||||
config: config, |
||||
backends: make(map[reflect.Type][]Backend), |
||||
updaters: subs, |
||||
updates: updates, |
||||
wallets: wallets, |
||||
quit: make(chan chan error), |
||||
} |
||||
for _, backend := range backends { |
||||
kind := reflect.TypeOf(backend) |
||||
am.backends[kind] = append(am.backends[kind], backend) |
||||
} |
||||
go am.update() |
||||
|
||||
return am |
||||
} |
||||
|
||||
// Close terminates the account manager's internal notification processes.
|
||||
func (am *Manager) Close() error { |
||||
errc := make(chan error) |
||||
am.quit <- errc |
||||
return <-errc |
||||
} |
||||
|
||||
// Config returns the configuration of account manager.
|
||||
func (am *Manager) Config() *Config { |
||||
return am.config |
||||
} |
||||
|
||||
// update is the wallet event loop listening for notifications from the backends
|
||||
// and updating the cache of wallets.
|
||||
func (am *Manager) update() { |
||||
// Close all subscriptions when the manager terminates
|
||||
defer func() { |
||||
am.lock.Lock() |
||||
for _, sub := range am.updaters { |
||||
sub.Unsubscribe() |
||||
} |
||||
am.updaters = nil |
||||
am.lock.Unlock() |
||||
}() |
||||
|
||||
// Loop until termination
|
||||
for { |
||||
select { |
||||
case event := <-am.updates: |
||||
// Wallet event arrived, update local cache
|
||||
am.lock.Lock() |
||||
switch event.Kind { |
||||
case WalletArrived: |
||||
am.wallets = merge(am.wallets, event.Wallet) |
||||
case WalletDropped: |
||||
am.wallets = drop(am.wallets, event.Wallet) |
||||
} |
||||
am.lock.Unlock() |
||||
|
||||
// Notify any listeners of the event
|
||||
am.feed.Send(event) |
||||
|
||||
case errc := <-am.quit: |
||||
// Manager terminating, return
|
||||
errc <- nil |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Backends retrieves the backend(s) with the given type from the account manager.
|
||||
func (am *Manager) Backends(kind reflect.Type) []Backend { |
||||
return am.backends[kind] |
||||
} |
||||
|
||||
// Wallets returns all signer accounts registered under this account manager.
|
||||
func (am *Manager) Wallets() []Wallet { |
||||
am.lock.RLock() |
||||
defer am.lock.RUnlock() |
||||
|
||||
cpy := make([]Wallet, len(am.wallets)) |
||||
copy(cpy, am.wallets) |
||||
return cpy |
||||
} |
||||
|
||||
// Wallet retrieves the wallet associated with a particular URL.
|
||||
func (am *Manager) Wallet(url string) (Wallet, error) { |
||||
am.lock.RLock() |
||||
defer am.lock.RUnlock() |
||||
|
||||
parsed, err := parseURL(url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for _, wallet := range am.Wallets() { |
||||
if wallet.URL() == parsed { |
||||
return wallet, nil |
||||
} |
||||
} |
||||
return nil, ErrUnknownWallet |
||||
} |
||||
|
||||
// Find attempts to locate the wallet corresponding to a specific account. Since
|
||||
// accounts can be dynamically added to and removed from wallets, this method has
|
||||
// a linear runtime in the number of wallets.
|
||||
func (am *Manager) Find(account Account) (Wallet, error) { |
||||
am.lock.RLock() |
||||
defer am.lock.RUnlock() |
||||
|
||||
for _, wallet := range am.wallets { |
||||
if wallet.Contains(account) { |
||||
return wallet, nil |
||||
} |
||||
} |
||||
return nil, ErrUnknownAccount |
||||
} |
||||
|
||||
// Subscribe creates an async subscription to receive notifications when the
|
||||
// manager detects the arrival or departure of a wallet from any of its backends.
|
||||
func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { |
||||
return am.feed.Subscribe(sink) |
||||
} |
||||
|
||||
// merge is a sorted analogue of append for wallets, where the ordering of the
|
||||
// origin list is preserved by inserting new wallets at the correct position.
|
||||
//
|
||||
// The original slice is assumed to be already sorted by URL.
|
||||
func merge(slice []Wallet, wallets ...Wallet) []Wallet { |
||||
for _, wallet := range wallets { |
||||
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) |
||||
if n == len(slice) { |
||||
slice = append(slice, wallet) |
||||
continue |
||||
} |
||||
slice = append(slice[:n], append([]Wallet{wallet}, slice[n:]...)...) |
||||
} |
||||
return slice |
||||
} |
||||
|
||||
// drop is the couterpart of merge, which looks up wallets from within the sorted
|
||||
// cache and removes the ones specified.
|
||||
func drop(slice []Wallet, wallets ...Wallet) []Wallet { |
||||
for _, wallet := range wallets { |
||||
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) |
||||
if n == len(slice) { |
||||
// Wallet not found, may happen during startup
|
||||
continue |
||||
} |
||||
slice = append(slice[:n], slice[n+1:]...) |
||||
} |
||||
return slice |
||||
} |
@ -0,0 +1,102 @@ |
||||
# Using the smartcard wallet |
||||
|
||||
## Requirements |
||||
|
||||
* A USB smartcard reader |
||||
* A keycard that supports the status app |
||||
* PCSCD version 4.3 running on your system **Only version 4.3 is currently supported** |
||||
|
||||
## Preparing the smartcard |
||||
|
||||
**WARNING: FOILLOWING THESE INSTRUCTIONS WILL DESTROY THE MASTER KEY ON YOUR CARD. ONLY PROCEED IF NO FUNDS ARE ASSOCIATED WITH THESE ACCOUNTS** |
||||
|
||||
You can use status' [keycard-cli](https://github.com/status-im/keycard-cli) and you should get _at least_ version 2.1.1 of their [smartcard application](https://github.com/status-im/status-keycard/releases/download/2.2.1/keycard_v2.2.1.cap) |
||||
|
||||
You also need to make sure that the PCSC daemon is running on your system. |
||||
|
||||
Then, you can install the application to the card by typing: |
||||
|
||||
``` |
||||
keycard install -a keycard_v2.2.1.cap && keycard init |
||||
``` |
||||
|
||||
At the end of this process, you will be provided with a PIN, a PUK and a pairing password. Write them down, you'll need them shortly. |
||||
|
||||
Start `geth` with the `console` command. You will notice the following warning: |
||||
|
||||
``` |
||||
WARN [04-09|16:58:38.898] Failed to open wallet url=pcsc://044def09 err="smartcard: pairing password needed" |
||||
``` |
||||
|
||||
Write down the URL (`pcsc://044def09` in this example). Then ask `geth` to open the wallet: |
||||
|
||||
``` |
||||
> personal.openWallet("pcsc://044def09") |
||||
Please enter the pairing password: |
||||
``` |
||||
|
||||
Enter the pairing password that you have received during card initialization. Same with the PIN that you will subsequently be |
||||
asked for. |
||||
|
||||
If everything goes well, you should see your new account when typing `personal` on the console: |
||||
|
||||
``` |
||||
> personal |
||||
WARN [04-09|17:02:07.330] Smartcard wallet account derivation failed url=pcsc://044def09 err="Unexpected response status Cla=0x80, Ins=0xd1, Sw=0x6985" |
||||
{ |
||||
listAccounts: [], |
||||
listWallets: [{ |
||||
status: "Empty, waiting for initialization", |
||||
url: "pcsc://044def09" |
||||
}], |
||||
... |
||||
} |
||||
``` |
||||
|
||||
So the communication with the card is working, but there is no key associated with this wallet. Let's create it: |
||||
|
||||
``` |
||||
> personal.initializeWallet("pcsc://044def09") |
||||
"tilt ... impact" |
||||
``` |
||||
|
||||
You should get a list of words, this is your seed so write them down. Your wallet should now be initialized: |
||||
|
||||
``` |
||||
> personal.listWallets |
||||
[{ |
||||
accounts: [{ |
||||
address: "0x678b7cd55c61917defb23546a41803c5bfefbc7a", |
||||
url: "pcsc://044d/m/44'/60'/0'/0/0" |
||||
}], |
||||
status: "Online", |
||||
url: "pcsc://044def09" |
||||
}] |
||||
``` |
||||
|
||||
You're all set! |
||||
|
||||
## Usage |
||||
|
||||
1. Start `geth` with the `console` command |
||||
2. Check the card's URL by checking `personal.listWallets`: |
||||
|
||||
``` |
||||
listWallets: [{ |
||||
status: "Online, can derive public keys", |
||||
url: "pcsc://a4d73015" |
||||
}] |
||||
``` |
||||
|
||||
3. Open the wallet, you will be prompted for your pairing password, then PIN: |
||||
|
||||
``` |
||||
personal.openWallet("pcsc://a4d73015") |
||||
``` |
||||
|
||||
4. Check that creation was successful by typing e.g. `personal`. Then use it like a regular wallet. |
||||
|
||||
## Known issues |
||||
|
||||
* Starting geth with a valid card seems to make firefox crash. |
||||
* PCSC version 4.4 should work, but is currently untested |
@ -0,0 +1,87 @@ |
||||
// 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 scwallet |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/binary" |
||||
"fmt" |
||||
) |
||||
|
||||
// commandAPDU represents an application data unit sent to a smartcard.
|
||||
type commandAPDU struct { |
||||
Cla, Ins, P1, P2 uint8 // Class, Instruction, Parameter 1, Parameter 2
|
||||
Data []byte // Command data
|
||||
Le uint8 // Command data length
|
||||
} |
||||
|
||||
// serialize serializes a command APDU.
|
||||
func (ca commandAPDU) serialize() ([]byte, error) { |
||||
buf := new(bytes.Buffer) |
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, ca.Cla); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := binary.Write(buf, binary.BigEndian, ca.Ins); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := binary.Write(buf, binary.BigEndian, ca.P1); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := binary.Write(buf, binary.BigEndian, ca.P2); err != nil { |
||||
return nil, err |
||||
} |
||||
if len(ca.Data) > 0 { |
||||
if err := binary.Write(buf, binary.BigEndian, uint8(len(ca.Data))); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := binary.Write(buf, binary.BigEndian, ca.Data); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
if err := binary.Write(buf, binary.BigEndian, ca.Le); err != nil { |
||||
return nil, err |
||||
} |
||||
return buf.Bytes(), nil |
||||
} |
||||
|
||||
// responseAPDU represents an application data unit received from a smart card.
|
||||
type responseAPDU struct { |
||||
Data []byte // response data
|
||||
Sw1, Sw2 uint8 // status words 1 and 2
|
||||
} |
||||
|
||||
// deserialize deserializes a response APDU.
|
||||
func (ra *responseAPDU) deserialize(data []byte) error { |
||||
if len(data) < 2 { |
||||
return fmt.Errorf("can not deserialize data: payload too short (%d < 2)", len(data)) |
||||
} |
||||
|
||||
ra.Data = make([]byte, len(data)-2) |
||||
|
||||
buf := bytes.NewReader(data) |
||||
if err := binary.Read(buf, binary.BigEndian, &ra.Data); err != nil { |
||||
return err |
||||
} |
||||
if err := binary.Read(buf, binary.BigEndian, &ra.Sw1); err != nil { |
||||
return err |
||||
} |
||||
if err := binary.Read(buf, binary.BigEndian, &ra.Sw2); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,302 @@ |
||||
// 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/>.
|
||||
|
||||
// This package implements support for smartcard-based hardware wallets such as
|
||||
// the one written by Status: https://github.com/status-im/hardware-wallet
|
||||
//
|
||||
// This implementation of smartcard wallets have a different interaction process
|
||||
// to other types of hardware wallet. The process works like this:
|
||||
//
|
||||
// 1. (First use with a given client) Establish a pairing between hardware
|
||||
// wallet and client. This requires a secret value called a 'pairing password'.
|
||||
// You can pair with an unpaired wallet with `personal.openWallet(URI, pairing password)`.
|
||||
// 2. (First use only) Initialize the wallet, which generates a keypair, stores
|
||||
// it on the wallet, and returns it so the user can back it up. You can
|
||||
// initialize a wallet with `personal.initializeWallet(URI)`.
|
||||
// 3. Connect to the wallet using the pairing information established in step 1.
|
||||
// You can connect to a paired wallet with `personal.openWallet(URI, PIN)`.
|
||||
// 4. Interact with the wallet as normal.
|
||||
|
||||
package scwallet |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"sort" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
pcsc "github.com/gballet/go-libpcsclite" |
||||
) |
||||
|
||||
// Scheme is the URI prefix for smartcard wallets.
|
||||
const Scheme = "pcsc" |
||||
|
||||
// refreshCycle is the maximum time between wallet refreshes (if USB hotplug
|
||||
// notifications don't work).
|
||||
const refreshCycle = time.Second |
||||
|
||||
// refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
|
||||
const refreshThrottling = 500 * time.Millisecond |
||||
|
||||
// smartcardPairing contains information about a smart card we have paired with
|
||||
// or might pair with the hub.
|
||||
type smartcardPairing struct { |
||||
PublicKey []byte `json:"publicKey"` |
||||
PairingIndex uint8 `json:"pairingIndex"` |
||||
PairingKey []byte `json:"pairingKey"` |
||||
Accounts map[common.Address]accounts.DerivationPath `json:"accounts"` |
||||
} |
||||
|
||||
// Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets.
|
||||
type Hub struct { |
||||
scheme string // Protocol scheme prefixing account and wallet URLs.
|
||||
|
||||
context *pcsc.Client |
||||
datadir string |
||||
pairings map[string]smartcardPairing |
||||
|
||||
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
||||
wallets map[string]*Wallet // Mapping from reader names to wallet instances
|
||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||
updating bool // Whether the event notification loop is running
|
||||
|
||||
quit chan chan error |
||||
|
||||
stateLock sync.RWMutex // Protects the internals of the hub from racey access
|
||||
} |
||||
|
||||
func (hub *Hub) readPairings() error { |
||||
hub.pairings = make(map[string]smartcardPairing) |
||||
pairingFile, err := os.Open(filepath.Join(hub.datadir, "smartcards.json")) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
pairingData, err := ioutil.ReadAll(pairingFile) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var pairings []smartcardPairing |
||||
if err := json.Unmarshal(pairingData, &pairings); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, pairing := range pairings { |
||||
hub.pairings[string(pairing.PublicKey)] = pairing |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (hub *Hub) writePairings() error { |
||||
pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pairingFile.Close() |
||||
|
||||
pairings := make([]smartcardPairing, 0, len(hub.pairings)) |
||||
for _, pairing := range hub.pairings { |
||||
pairings = append(pairings, pairing) |
||||
} |
||||
|
||||
pairingData, err := json.Marshal(pairings) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := pairingFile.Write(pairingData); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing { |
||||
if pairing, ok := hub.pairings[string(wallet.PublicKey)]; ok { |
||||
return &pairing |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (hub *Hub) setPairing(wallet *Wallet, pairing *smartcardPairing) error { |
||||
if pairing == nil { |
||||
delete(hub.pairings, string(wallet.PublicKey)) |
||||
} else { |
||||
hub.pairings[string(wallet.PublicKey)] = *pairing |
||||
} |
||||
return hub.writePairings() |
||||
} |
||||
|
||||
// NewHub creates a new hardware wallet manager for smartcards.
|
||||
func NewHub(scheme string, datadir string) (*Hub, error) { |
||||
context, err := pcsc.EstablishContext(pcsc.ScopeSystem) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
hub := &Hub{ |
||||
scheme: scheme, |
||||
context: context, |
||||
datadir: datadir, |
||||
wallets: make(map[string]*Wallet), |
||||
quit: make(chan chan error), |
||||
} |
||||
if err := hub.readPairings(); err != nil { |
||||
return nil, err |
||||
} |
||||
hub.refreshWallets() |
||||
return hub, nil |
||||
} |
||||
|
||||
// Wallets implements accounts.Backend, returning all the currently tracked smart
|
||||
// cards that appear to be hardware wallets.
|
||||
func (hub *Hub) Wallets() []accounts.Wallet { |
||||
// Make sure the list of wallets is up to date
|
||||
hub.refreshWallets() |
||||
|
||||
hub.stateLock.RLock() |
||||
defer hub.stateLock.RUnlock() |
||||
|
||||
cpy := make([]accounts.Wallet, 0, len(hub.wallets)) |
||||
for _, wallet := range hub.wallets { |
||||
cpy = append(cpy, wallet) |
||||
} |
||||
sort.Sort(accounts.WalletsByURL(cpy)) |
||||
return cpy |
||||
} |
||||
|
||||
// refreshWallets scans the devices attached to the machine and updates the
|
||||
// list of wallets based on the found devices.
|
||||
func (hub *Hub) refreshWallets() { |
||||
// Don't scan the USB like crazy it the user fetches wallets in a loop
|
||||
hub.stateLock.RLock() |
||||
elapsed := time.Since(hub.refreshed) |
||||
hub.stateLock.RUnlock() |
||||
|
||||
if elapsed < refreshThrottling { |
||||
return |
||||
} |
||||
// Retrieve all the smart card reader to check for cards
|
||||
readers, err := hub.context.ListReaders() |
||||
if err != nil { |
||||
// This is a perverted hack, the scard library returns an error if no card
|
||||
// readers are present instead of simply returning an empty list. We don't
|
||||
// want to fill the user's log with errors, so filter those out.
|
||||
if err.Error() != "scard: Cannot find a smart card reader." { |
||||
log.Error("Failed to enumerate smart card readers", "err", err) |
||||
return |
||||
} |
||||
} |
||||
// Transform the current list of wallets into the new one
|
||||
hub.stateLock.Lock() |
||||
|
||||
events := []accounts.WalletEvent{} |
||||
seen := make(map[string]struct{}) |
||||
|
||||
for _, reader := range readers { |
||||
// Mark the reader as present
|
||||
seen[reader] = struct{}{} |
||||
|
||||
// If we alreay know about this card, skip to the next reader, otherwise clean up
|
||||
if wallet, ok := hub.wallets[reader]; ok { |
||||
if err := wallet.ping(); err == nil { |
||||
continue |
||||
} |
||||
wallet.Close() |
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) |
||||
delete(hub.wallets, reader) |
||||
} |
||||
// New card detected, try to connect to it
|
||||
card, err := hub.context.Connect(reader, pcsc.ShareShared, pcsc.ProtocolAny) |
||||
if err != nil { |
||||
log.Debug("Failed to open smart card", "reader", reader, "err", err) |
||||
continue |
||||
} |
||||
wallet := NewWallet(hub, card) |
||||
if err = wallet.connect(); err != nil { |
||||
log.Debug("Failed to connect to smart card", "reader", reader, "err", err) |
||||
card.Disconnect(pcsc.LeaveCard) |
||||
continue |
||||
} |
||||
// Card connected, start tracking in amongs the wallets
|
||||
hub.wallets[reader] = wallet |
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) |
||||
} |
||||
// Remove any wallets no longer present
|
||||
for reader, wallet := range hub.wallets { |
||||
if _, ok := seen[reader]; !ok { |
||||
wallet.Close() |
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) |
||||
delete(hub.wallets, reader) |
||||
} |
||||
} |
||||
hub.refreshed = time.Now() |
||||
hub.stateLock.Unlock() |
||||
|
||||
for _, event := range events { |
||||
hub.updateFeed.Send(event) |
||||
} |
||||
} |
||||
|
||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||
// receive notifications on the addition or removal of smart card wallets.
|
||||
func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { |
||||
// We need the mutex to reliably start/stop the update loop
|
||||
hub.stateLock.Lock() |
||||
defer hub.stateLock.Unlock() |
||||
|
||||
// Subscribe the caller and track the subscriber count
|
||||
sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) |
||||
|
||||
// Subscribers require an active notification loop, start it
|
||||
if !hub.updating { |
||||
hub.updating = true |
||||
go hub.updater() |
||||
} |
||||
return sub |
||||
} |
||||
|
||||
// updater is responsible for maintaining an up-to-date list of wallets managed
|
||||
// by the smart card hub, and for firing wallet addition/removal events.
|
||||
func (hub *Hub) updater() { |
||||
for { |
||||
// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
|
||||
// <-hub.changes
|
||||
time.Sleep(refreshCycle) |
||||
|
||||
// Run the wallet refresher
|
||||
hub.refreshWallets() |
||||
|
||||
// If all our subscribers left, stop the updater
|
||||
hub.stateLock.Lock() |
||||
if hub.updateScope.Count() == 0 { |
||||
hub.updating = false |
||||
hub.stateLock.Unlock() |
||||
return |
||||
} |
||||
hub.stateLock.Unlock() |
||||
} |
||||
} |
@ -0,0 +1,346 @@ |
||||
// 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 scwallet |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/aes" |
||||
"crypto/cipher" |
||||
"crypto/rand" |
||||
"crypto/sha256" |
||||
"crypto/sha512" |
||||
"fmt" |
||||
|
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
pcsc "github.com/gballet/go-libpcsclite" |
||||
"github.com/wsddn/go-ecdh" |
||||
"golang.org/x/crypto/pbkdf2" |
||||
"golang.org/x/text/unicode/norm" |
||||
) |
||||
|
||||
const ( |
||||
maxPayloadSize = 223 |
||||
pairP1FirstStep = 0 |
||||
pairP1LastStep = 1 |
||||
|
||||
scSecretLength = 32 |
||||
scBlockSize = 16 |
||||
|
||||
insOpenSecureChannel = 0x10 |
||||
insMutuallyAuthenticate = 0x11 |
||||
insPair = 0x12 |
||||
insUnpair = 0x13 |
||||
|
||||
pairingSalt = "Keycard Pairing Password Salt" |
||||
) |
||||
|
||||
// SecureChannelSession enables secure communication with a hardware wallet.
|
||||
type SecureChannelSession struct { |
||||
card *pcsc.Card // A handle to the smartcard for communication
|
||||
secret []byte // A shared secret generated from our ECDSA keys
|
||||
publicKey []byte // Our own ephemeral public key
|
||||
PairingKey []byte // A permanent shared secret for a pairing, if present
|
||||
sessionEncKey []byte // The current session encryption key
|
||||
sessionMacKey []byte // The current session MAC key
|
||||
iv []byte // The current IV
|
||||
PairingIndex uint8 // The pairing index
|
||||
} |
||||
|
||||
// NewSecureChannelSession creates a new secure channel for the given card and public key.
|
||||
func NewSecureChannelSession(card *pcsc.Card, keyData []byte) (*SecureChannelSession, error) { |
||||
// Generate an ECDSA keypair for ourselves
|
||||
gen := ecdh.NewEllipticECDH(crypto.S256()) |
||||
private, public, err := gen.GenerateKey(rand.Reader) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
cardPublic, ok := gen.Unmarshal(keyData) |
||||
if !ok { |
||||
return nil, fmt.Errorf("Could not unmarshal public key from card") |
||||
} |
||||
|
||||
secret, err := gen.GenerateSharedSecret(private, cardPublic) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &SecureChannelSession{ |
||||
card: card, |
||||
secret: secret, |
||||
publicKey: gen.Marshal(public), |
||||
}, nil |
||||
} |
||||
|
||||
// Pair establishes a new pairing with the smartcard.
|
||||
func (s *SecureChannelSession) Pair(pairingPassword []byte) error { |
||||
secretHash := pbkdf2.Key(norm.NFKD.Bytes(pairingPassword), norm.NFKD.Bytes([]byte(pairingSalt)), 50000, 32, sha256.New) |
||||
|
||||
challenge := make([]byte, 32) |
||||
if _, err := rand.Read(challenge); err != nil { |
||||
return err |
||||
} |
||||
|
||||
response, err := s.pair(pairP1FirstStep, challenge) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
md := sha256.New() |
||||
md.Write(secretHash[:]) |
||||
md.Write(challenge) |
||||
|
||||
expectedCryptogram := md.Sum(nil) |
||||
cardCryptogram := response.Data[:32] |
||||
cardChallenge := response.Data[32:64] |
||||
|
||||
if !bytes.Equal(expectedCryptogram, cardCryptogram) { |
||||
return fmt.Errorf("Invalid card cryptogram %v != %v", expectedCryptogram, cardCryptogram) |
||||
} |
||||
|
||||
md.Reset() |
||||
md.Write(secretHash[:]) |
||||
md.Write(cardChallenge) |
||||
response, err = s.pair(pairP1LastStep, md.Sum(nil)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
md.Reset() |
||||
md.Write(secretHash[:]) |
||||
md.Write(response.Data[1:]) |
||||
s.PairingKey = md.Sum(nil) |
||||
s.PairingIndex = response.Data[0] |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Unpair disestablishes an existing pairing.
|
||||
func (s *SecureChannelSession) Unpair() error { |
||||
if s.PairingKey == nil { |
||||
return fmt.Errorf("Cannot unpair: not paired") |
||||
} |
||||
|
||||
_, err := s.transmitEncrypted(claSCWallet, insUnpair, s.PairingIndex, 0, []byte{}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
s.PairingKey = nil |
||||
// Close channel
|
||||
s.iv = nil |
||||
return nil |
||||
} |
||||
|
||||
// Open initializes the secure channel.
|
||||
func (s *SecureChannelSession) Open() error { |
||||
if s.iv != nil { |
||||
return fmt.Errorf("Session already opened") |
||||
} |
||||
|
||||
response, err := s.open() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Generate the encryption/mac key by hashing our shared secret,
|
||||
// pairing key, and the first bytes returned from the Open APDU.
|
||||
md := sha512.New() |
||||
md.Write(s.secret) |
||||
md.Write(s.PairingKey) |
||||
md.Write(response.Data[:scSecretLength]) |
||||
keyData := md.Sum(nil) |
||||
s.sessionEncKey = keyData[:scSecretLength] |
||||
s.sessionMacKey = keyData[scSecretLength : scSecretLength*2] |
||||
|
||||
// The IV is the last bytes returned from the Open APDU.
|
||||
s.iv = response.Data[scSecretLength:] |
||||
|
||||
return s.mutuallyAuthenticate() |
||||
} |
||||
|
||||
// mutuallyAuthenticate is an internal method to authenticate both ends of the
|
||||
// connection.
|
||||
func (s *SecureChannelSession) mutuallyAuthenticate() error { |
||||
data := make([]byte, scSecretLength) |
||||
if _, err := rand.Read(data); err != nil { |
||||
return err |
||||
} |
||||
|
||||
response, err := s.transmitEncrypted(claSCWallet, insMutuallyAuthenticate, 0, 0, data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if response.Sw1 != 0x90 || response.Sw2 != 0x00 { |
||||
return fmt.Errorf("Got unexpected response from MUTUALLY_AUTHENTICATE: 0x%x%x", response.Sw1, response.Sw2) |
||||
} |
||||
|
||||
if len(response.Data) != scSecretLength { |
||||
return fmt.Errorf("Response from MUTUALLY_AUTHENTICATE was %d bytes, expected %d", len(response.Data), scSecretLength) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// open is an internal method that sends an open APDU.
|
||||
func (s *SecureChannelSession) open() (*responseAPDU, error) { |
||||
return transmit(s.card, &commandAPDU{ |
||||
Cla: claSCWallet, |
||||
Ins: insOpenSecureChannel, |
||||
P1: s.PairingIndex, |
||||
P2: 0, |
||||
Data: s.publicKey, |
||||
Le: 0, |
||||
}) |
||||
} |
||||
|
||||
// pair is an internal method that sends a pair APDU.
|
||||
func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*responseAPDU, error) { |
||||
return transmit(s.card, &commandAPDU{ |
||||
Cla: claSCWallet, |
||||
Ins: insPair, |
||||
P1: p1, |
||||
P2: 0, |
||||
Data: data, |
||||
Le: 0, |
||||
}) |
||||
} |
||||
|
||||
// transmitEncrypted sends an encrypted message, and decrypts and returns the response.
|
||||
func (s *SecureChannelSession) transmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*responseAPDU, error) { |
||||
if s.iv == nil { |
||||
return nil, fmt.Errorf("Channel not open") |
||||
} |
||||
|
||||
data, err := s.encryptAPDU(data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
meta := [16]byte{cla, ins, p1, p2, byte(len(data) + scBlockSize)} |
||||
if err = s.updateIV(meta[:], data); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
fulldata := make([]byte, len(s.iv)+len(data)) |
||||
copy(fulldata, s.iv) |
||||
copy(fulldata[len(s.iv):], data) |
||||
|
||||
response, err := transmit(s.card, &commandAPDU{ |
||||
Cla: cla, |
||||
Ins: ins, |
||||
P1: p1, |
||||
P2: p2, |
||||
Data: fulldata, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
rmeta := [16]byte{byte(len(response.Data))} |
||||
rmac := response.Data[:len(s.iv)] |
||||
rdata := response.Data[len(s.iv):] |
||||
plainData, err := s.decryptAPDU(rdata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err = s.updateIV(rmeta[:], rdata); err != nil { |
||||
return nil, err |
||||
} |
||||
if !bytes.Equal(s.iv, rmac) { |
||||
return nil, fmt.Errorf("Invalid MAC in response") |
||||
} |
||||
|
||||
rapdu := &responseAPDU{} |
||||
rapdu.deserialize(plainData) |
||||
|
||||
if rapdu.Sw1 != sw1Ok { |
||||
return nil, fmt.Errorf("Unexpected response status Cla=0x%x, Ins=0x%x, Sw=0x%x%x", cla, ins, rapdu.Sw1, rapdu.Sw2) |
||||
} |
||||
|
||||
return rapdu, nil |
||||
} |
||||
|
||||
// encryptAPDU is an internal method that serializes and encrypts an APDU.
|
||||
func (s *SecureChannelSession) encryptAPDU(data []byte) ([]byte, error) { |
||||
if len(data) > maxPayloadSize { |
||||
return nil, fmt.Errorf("Payload of %d bytes exceeds maximum of %d", len(data), maxPayloadSize) |
||||
} |
||||
data = pad(data, 0x80) |
||||
|
||||
ret := make([]byte, len(data)) |
||||
|
||||
a, err := aes.NewCipher(s.sessionEncKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
crypter := cipher.NewCBCEncrypter(a, s.iv) |
||||
crypter.CryptBlocks(ret, data) |
||||
return ret, nil |
||||
} |
||||
|
||||
// pad applies message padding to a 16 byte boundary.
|
||||
func pad(data []byte, terminator byte) []byte { |
||||
padded := make([]byte, (len(data)/16+1)*16) |
||||
copy(padded, data) |
||||
padded[len(data)] = terminator |
||||
return padded |
||||
} |
||||
|
||||
// decryptAPDU is an internal method that decrypts and deserializes an APDU.
|
||||
func (s *SecureChannelSession) decryptAPDU(data []byte) ([]byte, error) { |
||||
a, err := aes.NewCipher(s.sessionEncKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ret := make([]byte, len(data)) |
||||
|
||||
crypter := cipher.NewCBCDecrypter(a, s.iv) |
||||
crypter.CryptBlocks(ret, data) |
||||
return unpad(ret, 0x80) |
||||
} |
||||
|
||||
// unpad strips padding from a message.
|
||||
func unpad(data []byte, terminator byte) ([]byte, error) { |
||||
for i := 1; i <= 16; i++ { |
||||
switch data[len(data)-i] { |
||||
case 0: |
||||
continue |
||||
case terminator: |
||||
return data[:len(data)-i], nil |
||||
default: |
||||
return nil, fmt.Errorf("Expected end of padding, got %d", data[len(data)-i]) |
||||
} |
||||
} |
||||
return nil, fmt.Errorf("Expected end of padding, got 0") |
||||
} |
||||
|
||||
// updateIV is an internal method that updates the initialization vector after
|
||||
// each message exchanged.
|
||||
func (s *SecureChannelSession) updateIV(meta, data []byte) error { |
||||
data = pad(data, 0) |
||||
a, err := aes.NewCipher(s.sessionMacKey) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
crypter := cipher.NewCBCEncrypter(a, make([]byte, 16)) |
||||
crypter.CryptBlocks(meta, meta) |
||||
crypter.CryptBlocks(data, data) |
||||
// The first 16 bytes of the last block is the MAC
|
||||
s.iv = data[len(data)-32 : len(data)-16] |
||||
return nil |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@ |
||||
// 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 accounts |
||||
|
||||
// AccountsByURL implements sort.Interface for []Account based on the URL field.
|
||||
type AccountsByURL []Account |
||||
|
||||
func (a AccountsByURL) Len() int { return len(a) } |
||||
func (a AccountsByURL) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
||||
func (a AccountsByURL) Less(i, j int) bool { return a[i].URL.Cmp(a[j].URL) < 0 } |
||||
|
||||
// WalletsByURL implements sort.Interface for []Wallet based on the URL field.
|
||||
type WalletsByURL []Wallet |
||||
|
||||
func (w WalletsByURL) Len() int { return len(w) } |
||||
func (w WalletsByURL) Swap(i, j int) { w[i], w[j] = w[j], w[i] } |
||||
func (w WalletsByURL) Less(i, j int) bool { return w[i].URL().Cmp(w[j].URL()) < 0 } |
@ -0,0 +1,104 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
// URL represents the canonical identification URL of a wallet or account.
|
||||
//
|
||||
// It is a simplified version of url.URL, with the important limitations (which
|
||||
// are considered features here) that it contains value-copyable components only,
|
||||
// as well as that it doesn't do any URL encoding/decoding of special characters.
|
||||
//
|
||||
// The former is important to allow an account to be copied without leaving live
|
||||
// references to the original version, whereas the latter is important to ensure
|
||||
// one single canonical form opposed to many allowed ones by the RFC 3986 spec.
|
||||
//
|
||||
// As such, these URLs should not be used outside of the scope of an Ethereum
|
||||
// wallet or account.
|
||||
type URL struct { |
||||
Scheme string // Protocol scheme to identify a capable account backend
|
||||
Path string // Path for the backend to identify a unique entity
|
||||
} |
||||
|
||||
// parseURL converts a user supplied URL into the accounts specific structure.
|
||||
func parseURL(url string) (URL, error) { |
||||
parts := strings.Split(url, "://") |
||||
if len(parts) != 2 || parts[0] == "" { |
||||
return URL{}, errors.New("protocol scheme missing") |
||||
} |
||||
return URL{ |
||||
Scheme: parts[0], |
||||
Path: parts[1], |
||||
}, nil |
||||
} |
||||
|
||||
// String implements the stringer interface.
|
||||
func (u URL) String() string { |
||||
if u.Scheme != "" { |
||||
return fmt.Sprintf("%s://%s", u.Scheme, u.Path) |
||||
} |
||||
return u.Path |
||||
} |
||||
|
||||
// TerminalString implements the log.TerminalStringer interface.
|
||||
func (u URL) TerminalString() string { |
||||
url := u.String() |
||||
if len(url) > 32 { |
||||
return url[:31] + "…" |
||||
} |
||||
return url |
||||
} |
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (u URL) MarshalJSON() ([]byte, error) { |
||||
return json.Marshal(u.String()) |
||||
} |
||||
|
||||
// UnmarshalJSON parses url.
|
||||
func (u *URL) UnmarshalJSON(input []byte) error { |
||||
var textURL string |
||||
err := json.Unmarshal(input, &textURL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
url, err := parseURL(textURL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
u.Scheme = url.Scheme |
||||
u.Path = url.Path |
||||
return nil |
||||
} |
||||
|
||||
// Cmp compares x and y and returns:
|
||||
//
|
||||
// -1 if x < y
|
||||
// 0 if x == y
|
||||
// +1 if x > y
|
||||
//
|
||||
func (u URL) Cmp(url URL) int { |
||||
if u.Scheme == url.Scheme { |
||||
return strings.Compare(u.Path, url.Path) |
||||
} |
||||
return strings.Compare(u.Scheme, url.Scheme) |
||||
} |
@ -0,0 +1,96 @@ |
||||
// 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 accounts |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestURLParsing(t *testing.T) { |
||||
url, err := parseURL("https://ethereum.org") |
||||
if err != nil { |
||||
t.Errorf("unexpected error: %v", err) |
||||
} |
||||
if url.Scheme != "https" { |
||||
t.Errorf("expected: %v, got: %v", "https", url.Scheme) |
||||
} |
||||
if url.Path != "ethereum.org" { |
||||
t.Errorf("expected: %v, got: %v", "ethereum.org", url.Path) |
||||
} |
||||
|
||||
_, err = parseURL("ethereum.org") |
||||
if err == nil { |
||||
t.Error("expected err, got: nil") |
||||
} |
||||
} |
||||
|
||||
func TestURLString(t *testing.T) { |
||||
url := URL{Scheme: "https", Path: "ethereum.org"} |
||||
if url.String() != "https://ethereum.org" { |
||||
t.Errorf("expected: %v, got: %v", "https://ethereum.org", url.String()) |
||||
} |
||||
|
||||
url = URL{Scheme: "", Path: "ethereum.org"} |
||||
if url.String() != "ethereum.org" { |
||||
t.Errorf("expected: %v, got: %v", "ethereum.org", url.String()) |
||||
} |
||||
} |
||||
|
||||
func TestURLMarshalJSON(t *testing.T) { |
||||
url := URL{Scheme: "https", Path: "ethereum.org"} |
||||
json, err := url.MarshalJSON() |
||||
if err != nil { |
||||
t.Errorf("unexpcted error: %v", err) |
||||
} |
||||
if string(json) != "\"https://ethereum.org\"" { |
||||
t.Errorf("expected: %v, got: %v", "\"https://ethereum.org\"", string(json)) |
||||
} |
||||
} |
||||
|
||||
func TestURLUnmarshalJSON(t *testing.T) { |
||||
url := &URL{} |
||||
err := url.UnmarshalJSON([]byte("\"https://ethereum.org\"")) |
||||
if err != nil { |
||||
t.Errorf("unexpcted error: %v", err) |
||||
} |
||||
if url.Scheme != "https" { |
||||
t.Errorf("expected: %v, got: %v", "https", url.Scheme) |
||||
} |
||||
if url.Path != "ethereum.org" { |
||||
t.Errorf("expected: %v, got: %v", "https", url.Path) |
||||
} |
||||
} |
||||
|
||||
func TestURLComparison(t *testing.T) { |
||||
tests := []struct { |
||||
urlA URL |
||||
urlB URL |
||||
expect int |
||||
}{ |
||||
{URL{"https", "ethereum.org"}, URL{"https", "ethereum.org"}, 0}, |
||||
{URL{"http", "ethereum.org"}, URL{"https", "ethereum.org"}, -1}, |
||||
{URL{"https", "ethereum.org/a"}, URL{"https", "ethereum.org"}, 1}, |
||||
{URL{"https", "abc.org"}, URL{"https", "ethereum.org"}, -1}, |
||||
} |
||||
|
||||
for i, tt := range tests { |
||||
result := tt.urlA.Cmp(tt.urlB) |
||||
if result != tt.expect { |
||||
t.Errorf("test %d: cmp mismatch: expected: %d, got: %d", i, tt.expect, result) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,240 @@ |
||||
// 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 usbwallet |
||||
|
||||
import ( |
||||
"errors" |
||||
"runtime" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/event" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/karalabe/hid" |
||||
) |
||||
|
||||
// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
const LedgerScheme = "ledger" |
||||
|
||||
// TrezorScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
const TrezorScheme = "trezor" |
||||
|
||||
// refreshCycle is the maximum time between wallet refreshes (if USB hotplug
|
||||
// notifications don't work).
|
||||
const refreshCycle = time.Second |
||||
|
||||
// refreshThrottling is the minimum time between wallet refreshes to avoid USB
|
||||
// trashing.
|
||||
const refreshThrottling = 500 * time.Millisecond |
||||
|
||||
// Hub is a accounts.Backend that can find and handle generic USB hardware wallets.
|
||||
type Hub struct { |
||||
scheme string // Protocol scheme prefixing account and wallet URLs.
|
||||
vendorID uint16 // USB vendor identifier used for device discovery
|
||||
productIDs []uint16 // USB product identifiers used for device discovery
|
||||
usageID uint16 // USB usage page identifier used for macOS device discovery
|
||||
endpointID int // USB endpoint identifier used for non-macOS device discovery
|
||||
makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver
|
||||
|
||||
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
||||
wallets []accounts.Wallet // List of USB wallet devices currently tracking
|
||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||
updating bool // Whether the event notification loop is running
|
||||
|
||||
quit chan chan error |
||||
|
||||
stateLock sync.RWMutex // Protects the internals of the hub from racey access
|
||||
|
||||
// TODO(karalabe): remove if hotplug lands on Windows
|
||||
commsPend int // Number of operations blocking enumeration
|
||||
commsLock sync.Mutex // Lock protecting the pending counter and enumeration
|
||||
} |
||||
|
||||
// NewLedgerHub creates a new hardware wallet manager for Ledger devices.
|
||||
func NewLedgerHub() (*Hub, error) { |
||||
return newHub(LedgerScheme, 0x2c97, []uint16{0x0000 /* Ledger Blue */, 0x0001 /* Ledger Nano S */}, 0xffa0, 0, newLedgerDriver) |
||||
} |
||||
|
||||
// NewTrezorHub creates a new hardware wallet manager for Trezor devices.
|
||||
func NewTrezorHub() (*Hub, error) { |
||||
return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor 1 */}, 0xff00, 0, newTrezorDriver) |
||||
} |
||||
|
||||
// newHub creates a new hardware wallet manager for generic USB devices.
|
||||
func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) { |
||||
if !hid.Supported() { |
||||
return nil, errors.New("unsupported platform") |
||||
} |
||||
hub := &Hub{ |
||||
scheme: scheme, |
||||
vendorID: vendorID, |
||||
productIDs: productIDs, |
||||
usageID: usageID, |
||||
endpointID: endpointID, |
||||
makeDriver: makeDriver, |
||||
quit: make(chan chan error), |
||||
} |
||||
hub.refreshWallets() |
||||
return hub, nil |
||||
} |
||||
|
||||
// Wallets implements accounts.Backend, returning all the currently tracked USB
|
||||
// devices that appear to be hardware wallets.
|
||||
func (hub *Hub) Wallets() []accounts.Wallet { |
||||
// Make sure the list of wallets is up to date
|
||||
hub.refreshWallets() |
||||
|
||||
hub.stateLock.RLock() |
||||
defer hub.stateLock.RUnlock() |
||||
|
||||
cpy := make([]accounts.Wallet, len(hub.wallets)) |
||||
copy(cpy, hub.wallets) |
||||
return cpy |
||||
} |
||||
|
||||
// refreshWallets scans the USB devices attached to the machine and updates the
|
||||
// list of wallets based on the found devices.
|
||||
func (hub *Hub) refreshWallets() { |
||||
// Don't scan the USB like crazy it the user fetches wallets in a loop
|
||||
hub.stateLock.RLock() |
||||
elapsed := time.Since(hub.refreshed) |
||||
hub.stateLock.RUnlock() |
||||
|
||||
if elapsed < refreshThrottling { |
||||
return |
||||
} |
||||
// Retrieve the current list of USB wallet devices
|
||||
var devices []hid.DeviceInfo |
||||
|
||||
if runtime.GOOS == "linux" { |
||||
// hidapi on Linux opens the device during enumeration to retrieve some infos,
|
||||
// breaking the Ledger protocol if that is waiting for user confirmation. This
|
||||
// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
|
||||
// need to prevent concurrent comms ourselves. The more elegant solution would
|
||||
// be to ditch enumeration in favor of hotplug events, but that don't work yet
|
||||
// on Windows so if we need to hack it anyway, this is more elegant for now.
|
||||
hub.commsLock.Lock() |
||||
if hub.commsPend > 0 { // A confirmation is pending, don't refresh
|
||||
hub.commsLock.Unlock() |
||||
return |
||||
} |
||||
} |
||||
for _, info := range hid.Enumerate(hub.vendorID, 0) { |
||||
for _, id := range hub.productIDs { |
||||
if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) { |
||||
devices = append(devices, info) |
||||
break |
||||
} |
||||
} |
||||
} |
||||
if runtime.GOOS == "linux" { |
||||
// See rationale before the enumeration why this is needed and only on Linux.
|
||||
hub.commsLock.Unlock() |
||||
} |
||||
// Transform the current list of wallets into the new one
|
||||
hub.stateLock.Lock() |
||||
|
||||
var ( |
||||
wallets = make([]accounts.Wallet, 0, len(devices)) |
||||
events []accounts.WalletEvent |
||||
) |
||||
|
||||
for _, device := range devices { |
||||
url := accounts.URL{Scheme: hub.scheme, Path: device.Path} |
||||
|
||||
// Drop wallets in front of the next device or those that failed for some reason
|
||||
for len(hub.wallets) > 0 { |
||||
// Abort if we're past the current device and found an operational one
|
||||
_, failure := hub.wallets[0].Status() |
||||
if hub.wallets[0].URL().Cmp(url) >= 0 || failure == nil { |
||||
break |
||||
} |
||||
// Drop the stale and failed devices
|
||||
events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Kind: accounts.WalletDropped}) |
||||
hub.wallets = hub.wallets[1:] |
||||
} |
||||
// If there are no more wallets or the device is before the next, wrap new wallet
|
||||
if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { |
||||
logger := log.New("url", url) |
||||
wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger} |
||||
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) |
||||
wallets = append(wallets, wallet) |
||||
continue |
||||
} |
||||
// If the device is the same as the first wallet, keep it
|
||||
if hub.wallets[0].URL().Cmp(url) == 0 { |
||||
wallets = append(wallets, hub.wallets[0]) |
||||
hub.wallets = hub.wallets[1:] |
||||
continue |
||||
} |
||||
} |
||||
// Drop any leftover wallets and set the new batch
|
||||
for _, wallet := range hub.wallets { |
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) |
||||
} |
||||
hub.refreshed = time.Now() |
||||
hub.wallets = wallets |
||||
hub.stateLock.Unlock() |
||||
|
||||
// Fire all wallet events and return
|
||||
for _, event := range events { |
||||
hub.updateFeed.Send(event) |
||||
} |
||||
} |
||||
|
||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||
// receive notifications on the addition or removal of USB wallets.
|
||||
func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { |
||||
// We need the mutex to reliably start/stop the update loop
|
||||
hub.stateLock.Lock() |
||||
defer hub.stateLock.Unlock() |
||||
|
||||
// Subscribe the caller and track the subscriber count
|
||||
sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) |
||||
|
||||
// Subscribers require an active notification loop, start it
|
||||
if !hub.updating { |
||||
hub.updating = true |
||||
go hub.updater() |
||||
} |
||||
return sub |
||||
} |
||||
|
||||
// updater is responsible for maintaining an up-to-date list of wallets managed
|
||||
// by the USB hub, and for firing wallet addition/removal events.
|
||||
func (hub *Hub) updater() { |
||||
for { |
||||
// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
|
||||
// <-hub.changes
|
||||
time.Sleep(refreshCycle) |
||||
|
||||
// Run the wallet refresher
|
||||
hub.refreshWallets() |
||||
|
||||
// If all our subscribers left, stop the updater
|
||||
hub.stateLock.Lock() |
||||
if hub.updateScope.Count() == 0 { |
||||
hub.updating = false |
||||
hub.stateLock.Unlock() |
||||
return |
||||
} |
||||
hub.stateLock.Unlock() |
||||
} |
||||
} |
@ -0,0 +1,464 @@ |
||||
// 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/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Ledger hardware
|
||||
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||
|
||||
package usbwallet |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"encoding/hex" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
|
||||
"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/log" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
// ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
|
||||
type ledgerOpcode byte |
||||
|
||||
// ledgerParam1 is an enumeration encoding the supported Ledger parameters for
|
||||
// specific opcodes. The same parameter values may be reused between opcodes.
|
||||
type ledgerParam1 byte |
||||
|
||||
// ledgerParam2 is an enumeration encoding the supported Ledger parameters for
|
||||
// specific opcodes. The same parameter values may be reused between opcodes.
|
||||
type ledgerParam2 byte |
||||
|
||||
const ( |
||||
ledgerOpRetrieveAddress ledgerOpcode = 0x02 // Returns the public key and Ethereum address for a given BIP 32 path
|
||||
ledgerOpSignTransaction ledgerOpcode = 0x04 // Signs an Ethereum transaction after having the user validate the parameters
|
||||
ledgerOpGetConfiguration ledgerOpcode = 0x06 // Returns specific wallet application configuration
|
||||
|
||||
ledgerP1DirectlyFetchAddress ledgerParam1 = 0x00 // Return address directly from the wallet
|
||||
ledgerP1InitTransactionData ledgerParam1 = 0x00 // First transaction data block for signing
|
||||
ledgerP1ContTransactionData ledgerParam1 = 0x80 // Subsequent transaction data block for signing
|
||||
ledgerP2DiscardAddressChainCode ledgerParam2 = 0x00 // Do not return the chain code along with the address
|
||||
) |
||||
|
||||
// errLedgerReplyInvalidHeader is the error message returned by a Ledger data exchange
|
||||
// if the device replies with a mismatching header. This usually means the device
|
||||
// is in browser mode.
|
||||
var errLedgerReplyInvalidHeader = errors.New("ledger: invalid reply header") |
||||
|
||||
// errLedgerInvalidVersionReply is the error message returned by a Ledger version retrieval
|
||||
// when a response does arrive, but it does not contain the expected data.
|
||||
var errLedgerInvalidVersionReply = errors.New("ledger: invalid version reply") |
||||
|
||||
// ledgerDriver implements the communication with a Ledger hardware wallet.
|
||||
type ledgerDriver struct { |
||||
device io.ReadWriter // USB device connection to communicate through
|
||||
version [3]byte // Current version of the Ledger firmware (zero if app is offline)
|
||||
browser bool // Flag whether the Ledger is in browser mode (reply channel mismatch)
|
||||
failure error // Any failure that would make the device unusable
|
||||
log log.Logger // Contextual logger to tag the ledger with its id
|
||||
} |
||||
|
||||
// newLedgerDriver creates a new instance of a Ledger USB protocol driver.
|
||||
func newLedgerDriver(logger log.Logger) driver { |
||||
return &ledgerDriver{ |
||||
log: logger, |
||||
} |
||||
} |
||||
|
||||
// Status implements usbwallet.driver, returning various states the Ledger can
|
||||
// currently be in.
|
||||
func (w *ledgerDriver) Status() (string, error) { |
||||
if w.failure != nil { |
||||
return fmt.Sprintf("Failed: %v", w.failure), w.failure |
||||
} |
||||
if w.browser { |
||||
return "Ethereum app in browser mode", w.failure |
||||
} |
||||
if w.offline() { |
||||
return "Ethereum app offline", w.failure |
||||
} |
||||
return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2]), w.failure |
||||
} |
||||
|
||||
// offline returns whether the wallet and the Ethereum app is offline or not.
|
||||
//
|
||||
// The method assumes that the state lock is held!
|
||||
func (w *ledgerDriver) offline() bool { |
||||
return w.version == [3]byte{0, 0, 0} |
||||
} |
||||
|
||||
// Open implements usbwallet.driver, attempting to initialize the connection to the
|
||||
// Ledger hardware wallet. The Ledger does not require a user passphrase, so that
|
||||
// parameter is silently discarded.
|
||||
func (w *ledgerDriver) Open(device io.ReadWriter, passphrase string) error { |
||||
w.device, w.failure = device, nil |
||||
|
||||
_, err := w.ledgerDerive(accounts.DefaultBaseDerivationPath) |
||||
if err != nil { |
||||
// Ethereum app is not running or in browser mode, nothing more to do, return
|
||||
if err == errLedgerReplyInvalidHeader { |
||||
w.browser = true |
||||
} |
||||
return nil |
||||
} |
||||
// Try to resolve the Ethereum app's version, will fail prior to v1.0.2
|
||||
if w.version, err = w.ledgerVersion(); err != nil { |
||||
w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1
|
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Close implements usbwallet.driver, cleaning up and metadata maintained within
|
||||
// the Ledger driver.
|
||||
func (w *ledgerDriver) Close() error { |
||||
w.browser, w.version = false, [3]byte{} |
||||
return nil |
||||
} |
||||
|
||||
// Heartbeat implements usbwallet.driver, performing a sanity check against the
|
||||
// Ledger to see if it's still online.
|
||||
func (w *ledgerDriver) Heartbeat() error { |
||||
if _, err := w.ledgerVersion(); err != nil && err != errLedgerInvalidVersionReply { |
||||
w.failure = err |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Derive implements usbwallet.driver, sending a derivation request to the Ledger
|
||||
// and returning the Ethereum address located on that derivation path.
|
||||
func (w *ledgerDriver) Derive(path accounts.DerivationPath) (common.Address, error) { |
||||
return w.ledgerDerive(path) |
||||
} |
||||
|
||||
// SignTx implements usbwallet.driver, sending the transaction to the Ledger and
|
||||
// waiting for the user to confirm or deny the transaction.
|
||||
//
|
||||
// Note, if the version of the Ethereum application running on the Ledger wallet is
|
||||
// too old to sign EIP-155 transactions, but such is requested nonetheless, an error
|
||||
// will be returned opposed to silently signing in Homestead mode.
|
||||
func (w *ledgerDriver) SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { |
||||
// If the Ethereum app doesn't run, abort
|
||||
if w.offline() { |
||||
return common.Address{}, nil, accounts.ErrWalletClosed |
||||
} |
||||
// Ensure the wallet is capable of signing the given transaction
|
||||
if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 { |
||||
return common.Address{}, nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", w.version[0], w.version[1], w.version[2]) |
||||
} |
||||
// All infos gathered and metadata checks out, request signing
|
||||
return w.ledgerSign(path, tx, chainID) |
||||
} |
||||
|
||||
// ledgerVersion retrieves the current version of the Ethereum wallet app running
|
||||
// on the Ledger wallet.
|
||||
//
|
||||
// The version retrieval protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+----+---
|
||||
// E0 | 06 | 00 | 00 | 00 | 04
|
||||
//
|
||||
// With no input data, and the output data being:
|
||||
//
|
||||
// Description | Length
|
||||
// ---------------------------------------------------+--------
|
||||
// Flags 01: arbitrary data signature enabled by user | 1 byte
|
||||
// Application major version | 1 byte
|
||||
// Application minor version | 1 byte
|
||||
// Application patch version | 1 byte
|
||||
func (w *ledgerDriver) ledgerVersion() ([3]byte, error) { |
||||
// Send the request and wait for the response
|
||||
reply, err := w.ledgerExchange(ledgerOpGetConfiguration, 0, 0, nil) |
||||
if err != nil { |
||||
return [3]byte{}, err |
||||
} |
||||
if len(reply) != 4 { |
||||
return [3]byte{}, errLedgerInvalidVersionReply |
||||
} |
||||
// Cache the version for future reference
|
||||
var version [3]byte |
||||
copy(version[:], reply[1:]) |
||||
return version, nil |
||||
} |
||||
|
||||
// ledgerDerive retrieves the currently active Ethereum address from a Ledger
|
||||
// wallet at the specified derivation path.
|
||||
//
|
||||
// The address derivation protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+-----+---
|
||||
// E0 | 02 | 00 return address
|
||||
// 01 display address and confirm before returning
|
||||
// | 00: do not return the chain code
|
||||
// | 01: return the chain code
|
||||
// | var | 00
|
||||
//
|
||||
// Where the input data is:
|
||||
//
|
||||
// Description | Length
|
||||
// -------------------------------------------------+--------
|
||||
// Number of BIP 32 derivations to perform (max 10) | 1 byte
|
||||
// First derivation index (big endian) | 4 bytes
|
||||
// ... | 4 bytes
|
||||
// Last derivation index (big endian) | 4 bytes
|
||||
//
|
||||
// And the output data is:
|
||||
//
|
||||
// Description | Length
|
||||
// ------------------------+-------------------
|
||||
// Public Key length | 1 byte
|
||||
// Uncompressed Public Key | arbitrary
|
||||
// Ethereum address length | 1 byte
|
||||
// Ethereum address | 40 bytes hex ascii
|
||||
// Chain code if requested | 32 bytes
|
||||
func (w *ledgerDriver) ledgerDerive(derivationPath []uint32) (common.Address, error) { |
||||
// Flatten the derivation path into the Ledger request
|
||||
path := make([]byte, 1+4*len(derivationPath)) |
||||
path[0] = byte(len(derivationPath)) |
||||
for i, component := range derivationPath { |
||||
binary.BigEndian.PutUint32(path[1+4*i:], component) |
||||
} |
||||
// Send the request and wait for the response
|
||||
reply, err := w.ledgerExchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) |
||||
if err != nil { |
||||
return common.Address{}, err |
||||
} |
||||
// Discard the public key, we don't need that for now
|
||||
if len(reply) < 1 || len(reply) < 1+int(reply[0]) { |
||||
return common.Address{}, errors.New("reply lacks public key entry") |
||||
} |
||||
reply = reply[1+int(reply[0]):] |
||||
|
||||
// Extract the Ethereum hex address string
|
||||
if len(reply) < 1 || len(reply) < 1+int(reply[0]) { |
||||
return common.Address{}, errors.New("reply lacks address entry") |
||||
} |
||||
hexstr := reply[1 : 1+int(reply[0])] |
||||
|
||||
// Decode the hex sting into an Ethereum address and return
|
||||
var address common.Address |
||||
if _, err = hex.Decode(address[:], hexstr); err != nil { |
||||
return common.Address{}, err |
||||
} |
||||
return address, nil |
||||
} |
||||
|
||||
// ledgerSign sends the transaction to the Ledger wallet, and waits for the user
|
||||
// to confirm or deny the transaction.
|
||||
//
|
||||
// The transaction signing protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+-----+---
|
||||
// E0 | 04 | 00: first transaction data block
|
||||
// 80: subsequent transaction data block
|
||||
// | 00 | variable | variable
|
||||
//
|
||||
// Where the input for the first transaction block (first 255 bytes) is:
|
||||
//
|
||||
// Description | Length
|
||||
// -------------------------------------------------+----------
|
||||
// Number of BIP 32 derivations to perform (max 10) | 1 byte
|
||||
// First derivation index (big endian) | 4 bytes
|
||||
// ... | 4 bytes
|
||||
// Last derivation index (big endian) | 4 bytes
|
||||
// RLP transaction chunk | arbitrary
|
||||
//
|
||||
// And the input for subsequent transaction blocks (first 255 bytes) are:
|
||||
//
|
||||
// Description | Length
|
||||
// ----------------------+----------
|
||||
// RLP transaction chunk | arbitrary
|
||||
//
|
||||
// And the output data is:
|
||||
//
|
||||
// Description | Length
|
||||
// ------------+---------
|
||||
// signature V | 1 byte
|
||||
// signature R | 32 bytes
|
||||
// signature S | 32 bytes
|
||||
func (w *ledgerDriver) ledgerSign(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { |
||||
// Flatten the derivation path into the Ledger request
|
||||
path := make([]byte, 1+4*len(derivationPath)) |
||||
path[0] = byte(len(derivationPath)) |
||||
for i, component := range derivationPath { |
||||
binary.BigEndian.PutUint32(path[1+4*i:], component) |
||||
} |
||||
// Create the transaction RLP based on whether legacy or EIP155 signing was requested
|
||||
var ( |
||||
txrlp []byte |
||||
err error |
||||
) |
||||
if chainID == nil { |
||||
if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
} else { |
||||
if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
} |
||||
payload := append(path, txrlp...) |
||||
|
||||
// Send the request and wait for the response
|
||||
var ( |
||||
op = ledgerP1InitTransactionData |
||||
reply []byte |
||||
) |
||||
for len(payload) > 0 { |
||||
// Calculate the size of the next data chunk
|
||||
chunk := 255 |
||||
if chunk > len(payload) { |
||||
chunk = len(payload) |
||||
} |
||||
// Send the chunk over, ensuring it's processed correctly
|
||||
reply, err = w.ledgerExchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) |
||||
if err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
// Shift the payload and ensure subsequent chunks are marked as such
|
||||
payload = payload[chunk:] |
||||
op = ledgerP1ContTransactionData |
||||
} |
||||
// Extract the Ethereum signature and do a sanity validation
|
||||
if len(reply) != 65 { |
||||
return common.Address{}, nil, errors.New("reply lacks signature") |
||||
} |
||||
signature := append(reply[1:], reply[0]) |
||||
|
||||
// Create the correct signer and signature transform based on the chain ID
|
||||
var signer types.Signer |
||||
if chainID == nil { |
||||
signer = new(types.HomesteadSigner) |
||||
} else { |
||||
signer = types.NewEIP155Signer(chainID) |
||||
signature[64] -= byte(chainID.Uint64()*2 + 35) |
||||
} |
||||
signed, err := tx.WithSignature(signer, signature) |
||||
if err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
sender, err := types.Sender(signer, signed) |
||||
if err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
return sender, signed, nil |
||||
} |
||||
|
||||
// ledgerExchange performs a data exchange with the Ledger wallet, sending it a
|
||||
// message and retrieving the response.
|
||||
//
|
||||
// The common transport header is defined as follows:
|
||||
//
|
||||
// Description | Length
|
||||
// --------------------------------------+----------
|
||||
// Communication channel ID (big endian) | 2 bytes
|
||||
// Command tag | 1 byte
|
||||
// Packet sequence index (big endian) | 2 bytes
|
||||
// Payload | arbitrary
|
||||
//
|
||||
// The Communication channel ID allows commands multiplexing over the same
|
||||
// physical link. It is not used for the time being, and should be set to 0101
|
||||
// to avoid compatibility issues with implementations ignoring a leading 00 byte.
|
||||
//
|
||||
// The Command tag describes the message content. Use TAG_APDU (0x05) for standard
|
||||
// APDU payloads, or TAG_PING (0x02) for a simple link test.
|
||||
//
|
||||
// The Packet sequence index describes the current sequence for fragmented payloads.
|
||||
// The first fragment index is 0x00.
|
||||
//
|
||||
// APDU Command payloads are encoded as follows:
|
||||
//
|
||||
// Description | Length
|
||||
// -----------------------------------
|
||||
// APDU length (big endian) | 2 bytes
|
||||
// APDU CLA | 1 byte
|
||||
// APDU INS | 1 byte
|
||||
// APDU P1 | 1 byte
|
||||
// APDU P2 | 1 byte
|
||||
// APDU length | 1 byte
|
||||
// Optional APDU data | arbitrary
|
||||
func (w *ledgerDriver) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { |
||||
// Construct the message payload, possibly split into multiple chunks
|
||||
apdu := make([]byte, 2, 7+len(data)) |
||||
|
||||
binary.BigEndian.PutUint16(apdu, uint16(5+len(data))) |
||||
apdu = append(apdu, []byte{0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))}...) |
||||
apdu = append(apdu, data...) |
||||
|
||||
// Stream all the chunks to the device
|
||||
header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended
|
||||
chunk := make([]byte, 64) |
||||
space := len(chunk) - len(header) |
||||
|
||||
for i := 0; len(apdu) > 0; i++ { |
||||
// Construct the new message to stream
|
||||
chunk = append(chunk[:0], header...) |
||||
binary.BigEndian.PutUint16(chunk[3:], uint16(i)) |
||||
|
||||
if len(apdu) > space { |
||||
chunk = append(chunk, apdu[:space]...) |
||||
apdu = apdu[space:] |
||||
} else { |
||||
chunk = append(chunk, apdu...) |
||||
apdu = nil |
||||
} |
||||
// Send over to the device
|
||||
w.log.Trace("Data chunk sent to the Ledger", "chunk", hexutil.Bytes(chunk)) |
||||
if _, err := w.device.Write(chunk); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
// Stream the reply back from the wallet in 64 byte chunks
|
||||
var reply []byte |
||||
chunk = chunk[:64] // Yeah, we surely have enough space
|
||||
for { |
||||
// Read the next chunk from the Ledger wallet
|
||||
if _, err := io.ReadFull(w.device, chunk); err != nil { |
||||
return nil, err |
||||
} |
||||
w.log.Trace("Data chunk received from the Ledger", "chunk", hexutil.Bytes(chunk)) |
||||
|
||||
// Make sure the transport header matches
|
||||
if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { |
||||
return nil, errLedgerReplyInvalidHeader |
||||
} |
||||
// If it's the first chunk, retrieve the total message length
|
||||
var payload []byte |
||||
|
||||
if chunk[3] == 0x00 && chunk[4] == 0x00 { |
||||
reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7]))) |
||||
payload = chunk[7:] |
||||
} else { |
||||
payload = chunk[5:] |
||||
} |
||||
// Append to the reply and stop when filled up
|
||||
if left := cap(reply) - len(reply); left > len(payload) { |
||||
reply = append(reply, payload...) |
||||
} else { |
||||
reply = append(reply, payload[:left]...) |
||||
break |
||||
} |
||||
} |
||||
return reply[:len(reply)-2], nil |
||||
} |
@ -0,0 +1,356 @@ |
||||
// 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/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Trezor hardware
|
||||
// wallets. The wire protocol spec can be found on the SatoshiLabs website:
|
||||
// https://doc.satoshilabs.com/trezor-tech/api-protobuf.html
|
||||
|
||||
package usbwallet |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/accounts/usbwallet/trezor" |
||||
"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/log" |
||||
"github.com/golang/protobuf/proto" |
||||
) |
||||
|
||||
// ErrTrezorPINNeeded is returned if opening the trezor requires a PIN code. In
|
||||
// this case, the calling application should display a pinpad and send back the
|
||||
// encoded passphrase.
|
||||
var ErrTrezorPINNeeded = errors.New("trezor: pin needed") |
||||
|
||||
// ErrTrezorPassphraseNeeded is returned if opening the trezor requires a passphrase
|
||||
var ErrTrezorPassphraseNeeded = errors.New("trezor: passphrase needed") |
||||
|
||||
// errTrezorReplyInvalidHeader is the error message returned by a Trezor data exchange
|
||||
// if the device replies with a mismatching header. This usually means the device
|
||||
// is in browser mode.
|
||||
var errTrezorReplyInvalidHeader = errors.New("trezor: invalid reply header") |
||||
|
||||
// trezorDriver implements the communication with a Trezor hardware wallet.
|
||||
type trezorDriver struct { |
||||
device io.ReadWriter // USB device connection to communicate through
|
||||
version [3]uint32 // Current version of the Trezor firmware
|
||||
label string // Current textual label of the Trezor device
|
||||
pinwait bool // Flags whether the device is waiting for PIN entry
|
||||
passphrasewait bool // Flags whether the device is waiting for passphrase entry
|
||||
failure error // Any failure that would make the device unusable
|
||||
log log.Logger // Contextual logger to tag the trezor with its id
|
||||
} |
||||
|
||||
// newTrezorDriver creates a new instance of a Trezor USB protocol driver.
|
||||
func newTrezorDriver(logger log.Logger) driver { |
||||
return &trezorDriver{ |
||||
log: logger, |
||||
} |
||||
} |
||||
|
||||
// Status implements accounts.Wallet, always whether the Trezor is opened, closed
|
||||
// or whether the Ethereum app was not started on it.
|
||||
func (w *trezorDriver) Status() (string, error) { |
||||
if w.failure != nil { |
||||
return fmt.Sprintf("Failed: %v", w.failure), w.failure |
||||
} |
||||
if w.device == nil { |
||||
return "Closed", w.failure |
||||
} |
||||
if w.pinwait { |
||||
return fmt.Sprintf("Trezor v%d.%d.%d '%s' waiting for PIN", w.version[0], w.version[1], w.version[2], w.label), w.failure |
||||
} |
||||
return fmt.Sprintf("Trezor v%d.%d.%d '%s' online", w.version[0], w.version[1], w.version[2], w.label), w.failure |
||||
} |
||||
|
||||
// Open implements usbwallet.driver, attempting to initialize the connection to
|
||||
// the Trezor hardware wallet. Initializing the Trezor is a two or three phase operation:
|
||||
// * The first phase is to initialize the connection and read the wallet's
|
||||
// features. This phase is invoked if the provided passphrase is empty. The
|
||||
// device will display the pinpad as a result and will return an appropriate
|
||||
// error to notify the user that a second open phase is needed.
|
||||
// * The second phase is to unlock access to the Trezor, which is done by the
|
||||
// user actually providing a passphrase mapping a keyboard keypad to the pin
|
||||
// number of the user (shuffled according to the pinpad displayed).
|
||||
// * If needed the device will ask for passphrase which will require calling
|
||||
// open again with the actual passphrase (3rd phase)
|
||||
func (w *trezorDriver) Open(device io.ReadWriter, passphrase string) error { |
||||
w.device, w.failure = device, nil |
||||
|
||||
// If phase 1 is requested, init the connection and wait for user callback
|
||||
if passphrase == "" && !w.passphrasewait { |
||||
// If we're already waiting for a PIN entry, insta-return
|
||||
if w.pinwait { |
||||
return ErrTrezorPINNeeded |
||||
} |
||||
// Initialize a connection to the device
|
||||
features := new(trezor.Features) |
||||
if _, err := w.trezorExchange(&trezor.Initialize{}, features); err != nil { |
||||
return err |
||||
} |
||||
w.version = [3]uint32{features.GetMajorVersion(), features.GetMinorVersion(), features.GetPatchVersion()} |
||||
w.label = features.GetLabel() |
||||
|
||||
// Do a manual ping, forcing the device to ask for its PIN and Passphrase
|
||||
askPin := true |
||||
askPassphrase := true |
||||
res, err := w.trezorExchange(&trezor.Ping{PinProtection: &askPin, PassphraseProtection: &askPassphrase}, new(trezor.PinMatrixRequest), new(trezor.PassphraseRequest), new(trezor.Success)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// Only return the PIN request if the device wasn't unlocked until now
|
||||
switch res { |
||||
case 0: |
||||
w.pinwait = true |
||||
return ErrTrezorPINNeeded |
||||
case 1: |
||||
w.pinwait = false |
||||
w.passphrasewait = true |
||||
return ErrTrezorPassphraseNeeded |
||||
case 2: |
||||
return nil // responded with trezor.Success
|
||||
} |
||||
} |
||||
// Phase 2 requested with actual PIN entry
|
||||
if w.pinwait { |
||||
w.pinwait = false |
||||
res, err := w.trezorExchange(&trezor.PinMatrixAck{Pin: &passphrase}, new(trezor.Success), new(trezor.PassphraseRequest)) |
||||
if err != nil { |
||||
w.failure = err |
||||
return err |
||||
} |
||||
if res == 1 { |
||||
w.passphrasewait = true |
||||
return ErrTrezorPassphraseNeeded |
||||
} |
||||
} else if w.passphrasewait { |
||||
w.passphrasewait = false |
||||
if _, err := w.trezorExchange(&trezor.PassphraseAck{Passphrase: &passphrase}, new(trezor.Success)); err != nil { |
||||
w.failure = err |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Close implements usbwallet.driver, cleaning up and metadata maintained within
|
||||
// the Trezor driver.
|
||||
func (w *trezorDriver) Close() error { |
||||
w.version, w.label, w.pinwait = [3]uint32{}, "", false |
||||
return nil |
||||
} |
||||
|
||||
// Heartbeat implements usbwallet.driver, performing a sanity check against the
|
||||
// Trezor to see if it's still online.
|
||||
func (w *trezorDriver) Heartbeat() error { |
||||
if _, err := w.trezorExchange(&trezor.Ping{}, new(trezor.Success)); err != nil { |
||||
w.failure = err |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Derive implements usbwallet.driver, sending a derivation request to the Trezor
|
||||
// and returning the Ethereum address located on that derivation path.
|
||||
func (w *trezorDriver) Derive(path accounts.DerivationPath) (common.Address, error) { |
||||
return w.trezorDerive(path) |
||||
} |
||||
|
||||
// SignTx implements usbwallet.driver, sending the transaction to the Trezor and
|
||||
// waiting for the user to confirm or deny the transaction.
|
||||
func (w *trezorDriver) SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { |
||||
if w.device == nil { |
||||
return common.Address{}, nil, accounts.ErrWalletClosed |
||||
} |
||||
return w.trezorSign(path, tx, chainID) |
||||
} |
||||
|
||||
// trezorDerive sends a derivation request to the Trezor device and returns the
|
||||
// Ethereum address located on that path.
|
||||
func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, error) { |
||||
address := new(trezor.EthereumAddress) |
||||
if _, err := w.trezorExchange(&trezor.EthereumGetAddress{AddressN: derivationPath}, address); err != nil { |
||||
return common.Address{}, err |
||||
} |
||||
return common.BytesToAddress(address.GetAddress()), nil |
||||
} |
||||
|
||||
// trezorSign sends the transaction to the Trezor wallet, and waits for the user
|
||||
// to confirm or deny the transaction.
|
||||
func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { |
||||
// Create the transaction initiation message
|
||||
data := tx.Data() |
||||
length := uint32(len(data)) |
||||
|
||||
request := &trezor.EthereumSignTx{ |
||||
AddressN: derivationPath, |
||||
Nonce: new(big.Int).SetUint64(tx.Nonce()).Bytes(), |
||||
GasPrice: tx.GasPrice().Bytes(), |
||||
GasLimit: new(big.Int).SetUint64(tx.Gas()).Bytes(), |
||||
Value: tx.Value().Bytes(), |
||||
DataLength: &length, |
||||
} |
||||
if to := tx.To(); to != nil { |
||||
request.To = (*to)[:] // Non contract deploy, set recipient explicitly
|
||||
} |
||||
if length > 1024 { // Send the data chunked if that was requested
|
||||
request.DataInitialChunk, data = data[:1024], data[1024:] |
||||
} else { |
||||
request.DataInitialChunk, data = data, nil |
||||
} |
||||
if chainID != nil { // EIP-155 transaction, set chain ID explicitly (only 32 bit is supported!?)
|
||||
id := uint32(chainID.Int64()) |
||||
request.ChainId = &id |
||||
} |
||||
// Send the initiation message and stream content until a signature is returned
|
||||
response := new(trezor.EthereumTxRequest) |
||||
if _, err := w.trezorExchange(request, response); err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
for response.DataLength != nil && int(*response.DataLength) <= len(data) { |
||||
chunk := data[:*response.DataLength] |
||||
data = data[*response.DataLength:] |
||||
|
||||
if _, err := w.trezorExchange(&trezor.EthereumTxAck{DataChunk: chunk}, response); err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
} |
||||
// Extract the Ethereum signature and do a sanity validation
|
||||
if len(response.GetSignatureR()) == 0 || len(response.GetSignatureS()) == 0 || response.GetSignatureV() == 0 { |
||||
return common.Address{}, nil, errors.New("reply lacks signature") |
||||
} |
||||
signature := append(append(response.GetSignatureR(), response.GetSignatureS()...), byte(response.GetSignatureV())) |
||||
|
||||
// Create the correct signer and signature transform based on the chain ID
|
||||
var signer types.Signer |
||||
if chainID == nil { |
||||
signer = new(types.HomesteadSigner) |
||||
} else { |
||||
signer = types.NewEIP155Signer(chainID) |
||||
signature[64] -= byte(chainID.Uint64()*2 + 35) |
||||
} |
||||
// Inject the final signature into the transaction and sanity check the sender
|
||||
signed, err := tx.WithSignature(signer, signature) |
||||
if err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
sender, err := types.Sender(signer, signed) |
||||
if err != nil { |
||||
return common.Address{}, nil, err |
||||
} |
||||
return sender, signed, nil |
||||
} |
||||
|
||||
// trezorExchange performs a data exchange with the Trezor wallet, sending it a
|
||||
// message and retrieving the response. If multiple responses are possible, the
|
||||
// method will also return the index of the destination object used.
|
||||
func (w *trezorDriver) trezorExchange(req proto.Message, results ...proto.Message) (int, error) { |
||||
// Construct the original message payload to chunk up
|
||||
data, err := proto.Marshal(req) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
payload := make([]byte, 8+len(data)) |
||||
copy(payload, []byte{0x23, 0x23}) |
||||
binary.BigEndian.PutUint16(payload[2:], trezor.Type(req)) |
||||
binary.BigEndian.PutUint32(payload[4:], uint32(len(data))) |
||||
copy(payload[8:], data) |
||||
|
||||
// Stream all the chunks to the device
|
||||
chunk := make([]byte, 64) |
||||
chunk[0] = 0x3f // Report ID magic number
|
||||
|
||||
for len(payload) > 0 { |
||||
// Construct the new message to stream, padding with zeroes if needed
|
||||
if len(payload) > 63 { |
||||
copy(chunk[1:], payload[:63]) |
||||
payload = payload[63:] |
||||
} else { |
||||
copy(chunk[1:], payload) |
||||
copy(chunk[1+len(payload):], make([]byte, 63-len(payload))) |
||||
payload = nil |
||||
} |
||||
// Send over to the device
|
||||
w.log.Trace("Data chunk sent to the Trezor", "chunk", hexutil.Bytes(chunk)) |
||||
if _, err := w.device.Write(chunk); err != nil { |
||||
return 0, err |
||||
} |
||||
} |
||||
// Stream the reply back from the wallet in 64 byte chunks
|
||||
var ( |
||||
kind uint16 |
||||
reply []byte |
||||
) |
||||
for { |
||||
// Read the next chunk from the Trezor wallet
|
||||
if _, err := io.ReadFull(w.device, chunk); err != nil { |
||||
return 0, err |
||||
} |
||||
w.log.Trace("Data chunk received from the Trezor", "chunk", hexutil.Bytes(chunk)) |
||||
|
||||
// Make sure the transport header matches
|
||||
if chunk[0] != 0x3f || (len(reply) == 0 && (chunk[1] != 0x23 || chunk[2] != 0x23)) { |
||||
return 0, errTrezorReplyInvalidHeader |
||||
} |
||||
// If it's the first chunk, retrieve the reply message type and total message length
|
||||
var payload []byte |
||||
|
||||
if len(reply) == 0 { |
||||
kind = binary.BigEndian.Uint16(chunk[3:5]) |
||||
reply = make([]byte, 0, int(binary.BigEndian.Uint32(chunk[5:9]))) |
||||
payload = chunk[9:] |
||||
} else { |
||||
payload = chunk[1:] |
||||
} |
||||
// Append to the reply and stop when filled up
|
||||
if left := cap(reply) - len(reply); left > len(payload) { |
||||
reply = append(reply, payload...) |
||||
} else { |
||||
reply = append(reply, payload[:left]...) |
||||
break |
||||
} |
||||
} |
||||
// Try to parse the reply into the requested reply message
|
||||
if kind == uint16(trezor.MessageType_MessageType_Failure) { |
||||
// Trezor returned a failure, extract and return the message
|
||||
failure := new(trezor.Failure) |
||||
if err := proto.Unmarshal(reply, failure); err != nil { |
||||
return 0, err |
||||
} |
||||
return 0, errors.New("trezor: " + failure.GetMessage()) |
||||
} |
||||
if kind == uint16(trezor.MessageType_MessageType_ButtonRequest) { |
||||
// Trezor is waiting for user confirmation, ack and wait for the next message
|
||||
return w.trezorExchange(&trezor.ButtonAck{}, results...) |
||||
} |
||||
for i, res := range results { |
||||
if trezor.Type(res) == kind { |
||||
return i, proto.Unmarshal(reply, res) |
||||
} |
||||
} |
||||
expected := make([]string, len(results)) |
||||
for i, res := range results { |
||||
expected[i] = trezor.Name(trezor.Type(res)) |
||||
} |
||||
return 0, fmt.Errorf("trezor: expected reply types %s, got %s", expected, trezor.Name(kind)) |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,905 @@ |
||||
// This file originates from the SatoshiLabs Trezor `common` repository at: |
||||
// https://github.com/trezor/trezor-common/blob/master/protob/messages.proto |
||||
// dated 28.07.2017, commit dd8ec3231fb5f7992360aff9bdfe30bb58130f4b. |
||||
|
||||
syntax = "proto2"; |
||||
|
||||
/** |
||||
* Messages for TREZOR communication |
||||
*/ |
||||
|
||||
// Sugar for easier handling in Java |
||||
option java_package = "com.satoshilabs.trezor.lib.protobuf"; |
||||
option java_outer_classname = "TrezorMessage"; |
||||
|
||||
import "types.proto"; |
||||
|
||||
/** |
||||
* Mapping between Trezor wire identifier (uint) and a protobuf message |
||||
*/ |
||||
enum MessageType { |
||||
MessageType_Initialize = 0 [(wire_in) = true]; |
||||
MessageType_Ping = 1 [(wire_in) = true]; |
||||
MessageType_Success = 2 [(wire_out) = true]; |
||||
MessageType_Failure = 3 [(wire_out) = true]; |
||||
MessageType_ChangePin = 4 [(wire_in) = true]; |
||||
MessageType_WipeDevice = 5 [(wire_in) = true]; |
||||
MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true]; |
||||
MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true]; |
||||
MessageType_FirmwareRequest = 8 [(wire_out) = true, (wire_bootloader) = true]; |
||||
MessageType_GetEntropy = 9 [(wire_in) = true]; |
||||
MessageType_Entropy = 10 [(wire_out) = true]; |
||||
MessageType_GetPublicKey = 11 [(wire_in) = true]; |
||||
MessageType_PublicKey = 12 [(wire_out) = true]; |
||||
MessageType_LoadDevice = 13 [(wire_in) = true]; |
||||
MessageType_ResetDevice = 14 [(wire_in) = true]; |
||||
MessageType_SignTx = 15 [(wire_in) = true]; |
||||
MessageType_SimpleSignTx = 16 [(wire_in) = true, deprecated = true]; |
||||
MessageType_Features = 17 [(wire_out) = true]; |
||||
MessageType_PinMatrixRequest = 18 [(wire_out) = true]; |
||||
MessageType_PinMatrixAck = 19 [(wire_in) = true, (wire_tiny) = true]; |
||||
MessageType_Cancel = 20 [(wire_in) = true]; |
||||
MessageType_TxRequest = 21 [(wire_out) = true]; |
||||
MessageType_TxAck = 22 [(wire_in) = true]; |
||||
MessageType_CipherKeyValue = 23 [(wire_in) = true]; |
||||
MessageType_ClearSession = 24 [(wire_in) = true]; |
||||
MessageType_ApplySettings = 25 [(wire_in) = true]; |
||||
MessageType_ButtonRequest = 26 [(wire_out) = true]; |
||||
MessageType_ButtonAck = 27 [(wire_in) = true, (wire_tiny) = true]; |
||||
MessageType_ApplyFlags = 28 [(wire_in) = true]; |
||||
MessageType_GetAddress = 29 [(wire_in) = true]; |
||||
MessageType_Address = 30 [(wire_out) = true]; |
||||
MessageType_SelfTest = 32 [(wire_in) = true, (wire_bootloader) = true]; |
||||
MessageType_BackupDevice = 34 [(wire_in) = true]; |
||||
MessageType_EntropyRequest = 35 [(wire_out) = true]; |
||||
MessageType_EntropyAck = 36 [(wire_in) = true]; |
||||
MessageType_SignMessage = 38 [(wire_in) = true]; |
||||
MessageType_VerifyMessage = 39 [(wire_in) = true]; |
||||
MessageType_MessageSignature = 40 [(wire_out) = true]; |
||||
MessageType_PassphraseRequest = 41 [(wire_out) = true]; |
||||
MessageType_PassphraseAck = 42 [(wire_in) = true, (wire_tiny) = true]; |
||||
MessageType_EstimateTxSize = 43 [(wire_in) = true, deprecated = true]; |
||||
MessageType_TxSize = 44 [(wire_out) = true, deprecated = true]; |
||||
MessageType_RecoveryDevice = 45 [(wire_in) = true]; |
||||
MessageType_WordRequest = 46 [(wire_out) = true]; |
||||
MessageType_WordAck = 47 [(wire_in) = true]; |
||||
MessageType_CipheredKeyValue = 48 [(wire_out) = true]; |
||||
MessageType_EncryptMessage = 49 [(wire_in) = true, deprecated = true]; |
||||
MessageType_EncryptedMessage = 50 [(wire_out) = true, deprecated = true]; |
||||
MessageType_DecryptMessage = 51 [(wire_in) = true, deprecated = true]; |
||||
MessageType_DecryptedMessage = 52 [(wire_out) = true, deprecated = true]; |
||||
MessageType_SignIdentity = 53 [(wire_in) = true]; |
||||
MessageType_SignedIdentity = 54 [(wire_out) = true]; |
||||
MessageType_GetFeatures = 55 [(wire_in) = true]; |
||||
MessageType_EthereumGetAddress = 56 [(wire_in) = true]; |
||||
MessageType_EthereumAddress = 57 [(wire_out) = true]; |
||||
MessageType_EthereumSignTx = 58 [(wire_in) = true]; |
||||
MessageType_EthereumTxRequest = 59 [(wire_out) = true]; |
||||
MessageType_EthereumTxAck = 60 [(wire_in) = true]; |
||||
MessageType_GetECDHSessionKey = 61 [(wire_in) = true]; |
||||
MessageType_ECDHSessionKey = 62 [(wire_out) = true]; |
||||
MessageType_SetU2FCounter = 63 [(wire_in) = true]; |
||||
MessageType_EthereumSignMessage = 64 [(wire_in) = true]; |
||||
MessageType_EthereumVerifyMessage = 65 [(wire_in) = true]; |
||||
MessageType_EthereumMessageSignature = 66 [(wire_out) = true]; |
||||
MessageType_DebugLinkDecision = 100 [(wire_debug_in) = true, (wire_tiny) = true]; |
||||
MessageType_DebugLinkGetState = 101 [(wire_debug_in) = true]; |
||||
MessageType_DebugLinkState = 102 [(wire_debug_out) = true]; |
||||
MessageType_DebugLinkStop = 103 [(wire_debug_in) = true]; |
||||
MessageType_DebugLinkLog = 104 [(wire_debug_out) = true]; |
||||
MessageType_DebugLinkMemoryRead = 110 [(wire_debug_in) = true]; |
||||
MessageType_DebugLinkMemory = 111 [(wire_debug_out) = true]; |
||||
MessageType_DebugLinkMemoryWrite = 112 [(wire_debug_in) = true]; |
||||
MessageType_DebugLinkFlashErase = 113 [(wire_debug_in) = true]; |
||||
} |
||||
|
||||
//////////////////// |
||||
// Basic messages // |
||||
//////////////////// |
||||
|
||||
/** |
||||
* Request: Reset device to default state and ask for device details |
||||
* @next Features |
||||
*/ |
||||
message Initialize { |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask for device details (no device reset) |
||||
* @next Features |
||||
*/ |
||||
message GetFeatures { |
||||
} |
||||
|
||||
/** |
||||
* Response: Reports various information about the device |
||||
* @prev Initialize |
||||
* @prev GetFeatures |
||||
*/ |
||||
message Features { |
||||
optional string vendor = 1; // name of the manufacturer, e.g. "bitcointrezor.com" |
||||
optional uint32 major_version = 2; // major version of the device, e.g. 1 |
||||
optional uint32 minor_version = 3; // minor version of the device, e.g. 0 |
||||
optional uint32 patch_version = 4; // patch version of the device, e.g. 0 |
||||
optional bool bootloader_mode = 5; // is device in bootloader mode? |
||||
optional string device_id = 6; // device's unique identifier |
||||
optional bool pin_protection = 7; // is device protected by PIN? |
||||
optional bool passphrase_protection = 8; // is node/mnemonic encrypted using passphrase? |
||||
optional string language = 9; // device language |
||||
optional string label = 10; // device description label |
||||
repeated CoinType coins = 11; // supported coins |
||||
optional bool initialized = 12; // does device contain seed? |
||||
optional bytes revision = 13; // SCM revision of firmware |
||||
optional bytes bootloader_hash = 14; // hash of the bootloader |
||||
optional bool imported = 15; // was storage imported from an external source? |
||||
optional bool pin_cached = 16; // is PIN already cached in session? |
||||
optional bool passphrase_cached = 17; // is passphrase already cached in session? |
||||
optional bool firmware_present = 18; // is valid firmware loaded? |
||||
optional bool needs_backup = 19; // does storage need backup? (equals to Storage.needs_backup) |
||||
optional uint32 flags = 20; // device flags (equals to Storage.flags) |
||||
} |
||||
|
||||
/** |
||||
* Request: clear session (removes cached PIN, passphrase, etc). |
||||
* @next Success |
||||
*/ |
||||
message ClearSession { |
||||
} |
||||
|
||||
/** |
||||
* Request: change language and/or label of the device |
||||
* @next Success |
||||
* @next Failure |
||||
* @next ButtonRequest |
||||
* @next PinMatrixRequest |
||||
*/ |
||||
message ApplySettings { |
||||
optional string language = 1; |
||||
optional string label = 2; |
||||
optional bool use_passphrase = 3; |
||||
optional bytes homescreen = 4; |
||||
} |
||||
|
||||
/** |
||||
* Request: set flags of the device |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message ApplyFlags { |
||||
optional uint32 flags = 1; // bitmask, can only set bits, not unset |
||||
} |
||||
|
||||
/** |
||||
* Request: Starts workflow for setting/changing/removing the PIN |
||||
* @next ButtonRequest |
||||
* @next PinMatrixRequest |
||||
*/ |
||||
message ChangePin { |
||||
optional bool remove = 1; // is PIN removal requested? |
||||
} |
||||
|
||||
/** |
||||
* Request: Test if the device is alive, device sends back the message in Success response |
||||
* @next Success |
||||
*/ |
||||
message Ping { |
||||
optional string message = 1; // message to send back in Success message |
||||
optional bool button_protection = 2; // ask for button press |
||||
optional bool pin_protection = 3; // ask for PIN if set in device |
||||
optional bool passphrase_protection = 4; // ask for passphrase if set in device |
||||
} |
||||
|
||||
/** |
||||
* Response: Success of the previous request |
||||
*/ |
||||
message Success { |
||||
optional string message = 1; // human readable description of action or request-specific payload |
||||
} |
||||
|
||||
/** |
||||
* Response: Failure of the previous request |
||||
*/ |
||||
message Failure { |
||||
optional FailureType code = 1; // computer-readable definition of the error state |
||||
optional string message = 2; // human-readable message of the error state |
||||
} |
||||
|
||||
/** |
||||
* Response: Device is waiting for HW button press. |
||||
* @next ButtonAck |
||||
* @next Cancel |
||||
*/ |
||||
message ButtonRequest { |
||||
optional ButtonRequestType code = 1; |
||||
optional string data = 2; |
||||
} |
||||
|
||||
/** |
||||
* Request: Computer agrees to wait for HW button press |
||||
* @prev ButtonRequest |
||||
*/ |
||||
message ButtonAck { |
||||
} |
||||
|
||||
/** |
||||
* Response: Device is asking computer to show PIN matrix and awaits PIN encoded using this matrix scheme |
||||
* @next PinMatrixAck |
||||
* @next Cancel |
||||
*/ |
||||
message PinMatrixRequest { |
||||
optional PinMatrixRequestType type = 1; |
||||
} |
||||
|
||||
/** |
||||
* Request: Computer responds with encoded PIN |
||||
* @prev PinMatrixRequest |
||||
*/ |
||||
message PinMatrixAck { |
||||
required string pin = 1; // matrix encoded PIN entered by user |
||||
} |
||||
|
||||
/** |
||||
* Request: Abort last operation that required user interaction |
||||
* @prev ButtonRequest |
||||
* @prev PinMatrixRequest |
||||
* @prev PassphraseRequest |
||||
*/ |
||||
message Cancel { |
||||
} |
||||
|
||||
/** |
||||
* Response: Device awaits encryption passphrase |
||||
* @next PassphraseAck |
||||
* @next Cancel |
||||
*/ |
||||
message PassphraseRequest { |
||||
} |
||||
|
||||
/** |
||||
* Request: Send passphrase back |
||||
* @prev PassphraseRequest |
||||
*/ |
||||
message PassphraseAck { |
||||
required string passphrase = 1; |
||||
} |
||||
|
||||
/** |
||||
* Request: Request a sample of random data generated by hardware RNG. May be used for testing. |
||||
* @next ButtonRequest |
||||
* @next Entropy |
||||
* @next Failure |
||||
*/ |
||||
message GetEntropy { |
||||
required uint32 size = 1; // size of requested entropy |
||||
} |
||||
|
||||
/** |
||||
* Response: Reply with random data generated by internal RNG |
||||
* @prev GetEntropy |
||||
*/ |
||||
message Entropy { |
||||
required bytes entropy = 1; // stream of random generated bytes |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device for public key corresponding to address_n path |
||||
* @next PassphraseRequest |
||||
* @next PublicKey |
||||
* @next Failure |
||||
*/ |
||||
message GetPublicKey { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
optional string ecdsa_curve_name = 2; // ECDSA curve name to use |
||||
optional bool show_display = 3; // optionally show on display before sending the result |
||||
optional string coin_name = 4 [default='Bitcoin']; |
||||
} |
||||
|
||||
/** |
||||
* Response: Contains public key derived from device private seed |
||||
* @prev GetPublicKey |
||||
*/ |
||||
message PublicKey { |
||||
required HDNodeType node = 1; // BIP32 public node |
||||
optional string xpub = 2; // serialized form of public node |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device for address corresponding to address_n path |
||||
* @next PassphraseRequest |
||||
* @next Address |
||||
* @next Failure |
||||
*/ |
||||
message GetAddress { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
optional string coin_name = 2 [default='Bitcoin']; |
||||
optional bool show_display = 3 ; // optionally show on display before sending the result |
||||
optional MultisigRedeemScriptType multisig = 4; // filled if we are showing a multisig address |
||||
optional InputScriptType script_type = 5 [default=SPENDADDRESS]; // used to distinguish between various address formats (non-segwit, segwit, etc.) |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device for Ethereum address corresponding to address_n path |
||||
* @next PassphraseRequest |
||||
* @next EthereumAddress |
||||
* @next Failure |
||||
*/ |
||||
message EthereumGetAddress { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
optional bool show_display = 2; // optionally show on display before sending the result |
||||
} |
||||
|
||||
/** |
||||
* Response: Contains address derived from device private seed |
||||
* @prev GetAddress |
||||
*/ |
||||
message Address { |
||||
required string address = 1; // Coin address in Base58 encoding |
||||
} |
||||
|
||||
/** |
||||
* Response: Contains an Ethereum address derived from device private seed |
||||
* @prev EthereumGetAddress |
||||
*/ |
||||
message EthereumAddress { |
||||
required bytes address = 1; // Coin address as an Ethereum 160 bit hash |
||||
} |
||||
|
||||
/** |
||||
* Request: Request device to wipe all sensitive data and settings |
||||
* @next ButtonRequest |
||||
*/ |
||||
message WipeDevice { |
||||
} |
||||
|
||||
/** |
||||
* Request: Load seed and related internal settings from the computer |
||||
* @next ButtonRequest |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message LoadDevice { |
||||
optional string mnemonic = 1; // seed encoded as BIP-39 mnemonic (12, 18 or 24 words) |
||||
optional HDNodeType node = 2; // BIP-32 node |
||||
optional string pin = 3; // set PIN protection |
||||
optional bool passphrase_protection = 4; // enable master node encryption using passphrase |
||||
optional string language = 5 [default='english']; // device language |
||||
optional string label = 6; // device label |
||||
optional bool skip_checksum = 7; // do not test mnemonic for valid BIP-39 checksum |
||||
optional uint32 u2f_counter = 8; // U2F counter |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to do initialization involving user interaction |
||||
* @next EntropyRequest |
||||
* @next Failure |
||||
*/ |
||||
message ResetDevice { |
||||
optional bool display_random = 1; // display entropy generated by the device before asking for additional entropy |
||||
optional uint32 strength = 2 [default=256]; // strength of seed in bits |
||||
optional bool passphrase_protection = 3; // enable master node encryption using passphrase |
||||
optional bool pin_protection = 4; // enable PIN protection |
||||
optional string language = 5 [default='english']; // device language |
||||
optional string label = 6; // device label |
||||
optional uint32 u2f_counter = 7; // U2F counter |
||||
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow |
||||
} |
||||
|
||||
/** |
||||
* Request: Perform backup of the device seed if not backed up using ResetDevice |
||||
* @next ButtonRequest |
||||
*/ |
||||
message BackupDevice { |
||||
} |
||||
|
||||
/** |
||||
* Response: Ask for additional entropy from host computer |
||||
* @prev ResetDevice |
||||
* @next EntropyAck |
||||
*/ |
||||
message EntropyRequest { |
||||
} |
||||
|
||||
/** |
||||
* Request: Provide additional entropy for seed generation function |
||||
* @prev EntropyRequest |
||||
* @next ButtonRequest |
||||
*/ |
||||
message EntropyAck { |
||||
optional bytes entropy = 1; // 256 bits (32 bytes) of random data |
||||
} |
||||
|
||||
/** |
||||
* Request: Start recovery workflow asking user for specific words of mnemonic |
||||
* Used to recovery device safely even on untrusted computer. |
||||
* @next WordRequest |
||||
*/ |
||||
message RecoveryDevice { |
||||
optional uint32 word_count = 1; // number of words in BIP-39 mnemonic |
||||
optional bool passphrase_protection = 2; // enable master node encryption using passphrase |
||||
optional bool pin_protection = 3; // enable PIN protection |
||||
optional string language = 4 [default='english']; // device language |
||||
optional string label = 5; // device label |
||||
optional bool enforce_wordlist = 6; // enforce BIP-39 wordlist during the process |
||||
// 7 reserved for unused recovery method |
||||
optional uint32 type = 8; // supported recovery type (see RecoveryType) |
||||
optional uint32 u2f_counter = 9; // U2F counter |
||||
optional bool dry_run = 10; // perform dry-run recovery workflow (for safe mnemonic validation) |
||||
} |
||||
|
||||
/** |
||||
* Response: Device is waiting for user to enter word of the mnemonic |
||||
* Its position is shown only on device's internal display. |
||||
* @prev RecoveryDevice |
||||
* @prev WordAck |
||||
*/ |
||||
message WordRequest { |
||||
optional WordRequestType type = 1; |
||||
} |
||||
|
||||
/** |
||||
* Request: Computer replies with word from the mnemonic |
||||
* @prev WordRequest |
||||
* @next WordRequest |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message WordAck { |
||||
required string word = 1; // one word of mnemonic on asked position |
||||
} |
||||
|
||||
////////////////////////////// |
||||
// Message signing messages // |
||||
////////////////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to sign message |
||||
* @next MessageSignature |
||||
* @next Failure |
||||
*/ |
||||
message SignMessage { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
required bytes message = 2; // message to be signed |
||||
optional string coin_name = 3 [default='Bitcoin']; // coin to use for signing |
||||
optional InputScriptType script_type = 4 [default=SPENDADDRESS]; // used to distinguish between various address formats (non-segwit, segwit, etc.) |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to verify message |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message VerifyMessage { |
||||
optional string address = 1; // address to verify |
||||
optional bytes signature = 2; // signature to verify |
||||
optional bytes message = 3; // message to verify |
||||
optional string coin_name = 4 [default='Bitcoin']; // coin to use for verifying |
||||
} |
||||
|
||||
/** |
||||
* Response: Signed message |
||||
* @prev SignMessage |
||||
*/ |
||||
message MessageSignature { |
||||
optional string address = 1; // address used to sign the message |
||||
optional bytes signature = 2; // signature of the message |
||||
} |
||||
|
||||
/////////////////////////// |
||||
// Encryption/decryption // |
||||
/////////////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to encrypt message |
||||
* @next EncryptedMessage |
||||
* @next Failure |
||||
*/ |
||||
message EncryptMessage { |
||||
optional bytes pubkey = 1; // public key |
||||
optional bytes message = 2; // message to encrypt |
||||
optional bool display_only = 3; // show just on display? (don't send back via wire) |
||||
repeated uint32 address_n = 4; // BIP-32 path to derive the signing key from master node |
||||
optional string coin_name = 5 [default='Bitcoin']; // coin to use for signing |
||||
} |
||||
|
||||
/** |
||||
* Response: Encrypted message |
||||
* @prev EncryptMessage |
||||
*/ |
||||
message EncryptedMessage { |
||||
optional bytes nonce = 1; // nonce used during encryption |
||||
optional bytes message = 2; // encrypted message |
||||
optional bytes hmac = 3; // message hmac |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to decrypt message |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message DecryptMessage { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the decryption key from master node |
||||
optional bytes nonce = 2; // nonce used during encryption |
||||
optional bytes message = 3; // message to decrypt |
||||
optional bytes hmac = 4; // message hmac |
||||
} |
||||
|
||||
/** |
||||
* Response: Decrypted message |
||||
* @prev DecryptedMessage |
||||
*/ |
||||
message DecryptedMessage { |
||||
optional bytes message = 1; // decrypted message |
||||
optional string address = 2; // address used to sign the message (if used) |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to encrypt or decrypt value of given key |
||||
* @next CipheredKeyValue |
||||
* @next Failure |
||||
*/ |
||||
message CipherKeyValue { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
optional string key = 2; // key component of key:value |
||||
optional bytes value = 3; // value component of key:value |
||||
optional bool encrypt = 4; // are we encrypting (True) or decrypting (False)? |
||||
optional bool ask_on_encrypt = 5; // should we ask on encrypt operation? |
||||
optional bool ask_on_decrypt = 6; // should we ask on decrypt operation? |
||||
optional bytes iv = 7; // initialization vector (will be computed if not set) |
||||
} |
||||
|
||||
/** |
||||
* Response: Return ciphered/deciphered value |
||||
* @prev CipherKeyValue |
||||
*/ |
||||
message CipheredKeyValue { |
||||
optional bytes value = 1; // ciphered/deciphered value |
||||
} |
||||
|
||||
////////////////////////////////// |
||||
// Transaction signing messages // |
||||
////////////////////////////////// |
||||
|
||||
/** |
||||
* Request: Estimated size of the transaction |
||||
* This behaves exactly like SignTx, which means that it can ask using TxRequest |
||||
* This call is non-blocking (except possible PassphraseRequest to unlock the seed) |
||||
* @next TxSize |
||||
* @next Failure |
||||
*/ |
||||
message EstimateTxSize { |
||||
required uint32 outputs_count = 1; // number of transaction outputs |
||||
required uint32 inputs_count = 2; // number of transaction inputs |
||||
optional string coin_name = 3 [default='Bitcoin']; // coin to use |
||||
} |
||||
|
||||
/** |
||||
* Response: Estimated size of the transaction |
||||
* @prev EstimateTxSize |
||||
*/ |
||||
message TxSize { |
||||
optional uint32 tx_size = 1; // estimated size of transaction in bytes |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to sign transaction |
||||
* @next PassphraseRequest |
||||
* @next PinMatrixRequest |
||||
* @next TxRequest |
||||
* @next Failure |
||||
*/ |
||||
message SignTx { |
||||
required uint32 outputs_count = 1; // number of transaction outputs |
||||
required uint32 inputs_count = 2; // number of transaction inputs |
||||
optional string coin_name = 3 [default='Bitcoin']; // coin to use |
||||
optional uint32 version = 4 [default=1]; // transaction version |
||||
optional uint32 lock_time = 5 [default=0]; // transaction lock_time |
||||
} |
||||
|
||||
/** |
||||
* Request: Simplified transaction signing |
||||
* This method doesn't support streaming, so there are hardware limits in number of inputs and outputs. |
||||
* In case of success, the result is returned using TxRequest message. |
||||
* @next PassphraseRequest |
||||
* @next PinMatrixRequest |
||||
* @next TxRequest |
||||
* @next Failure |
||||
*/ |
||||
message SimpleSignTx { |
||||
repeated TxInputType inputs = 1; // transaction inputs |
||||
repeated TxOutputType outputs = 2; // transaction outputs |
||||
repeated TransactionType transactions = 3; // transactions whose outputs are used to build current inputs |
||||
optional string coin_name = 4 [default='Bitcoin']; // coin to use |
||||
optional uint32 version = 5 [default=1]; // transaction version |
||||
optional uint32 lock_time = 6 [default=0]; // transaction lock_time |
||||
} |
||||
|
||||
/** |
||||
* Response: Device asks for information for signing transaction or returns the last result |
||||
* If request_index is set, device awaits TxAck message (with fields filled in according to request_type) |
||||
* If signature_index is set, 'signature' contains signed input of signature_index's input |
||||
* @prev SignTx |
||||
* @prev SimpleSignTx |
||||
* @prev TxAck |
||||
*/ |
||||
message TxRequest { |
||||
optional RequestType request_type = 1; // what should be filled in TxAck message? |
||||
optional TxRequestDetailsType details = 2; // request for tx details |
||||
optional TxRequestSerializedType serialized = 3; // serialized data and request for next |
||||
} |
||||
|
||||
/** |
||||
* Request: Reported transaction data |
||||
* @prev TxRequest |
||||
* @next TxRequest |
||||
*/ |
||||
message TxAck { |
||||
optional TransactionType tx = 1; |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to sign transaction |
||||
* All fields are optional from the protocol's point of view. Each field defaults to value `0` if missing. |
||||
* Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. |
||||
* @next PassphraseRequest |
||||
* @next PinMatrixRequest |
||||
* @next EthereumTxRequest |
||||
* @next Failure |
||||
*/ |
||||
message EthereumSignTx { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
optional bytes nonce = 2; // <=256 bit unsigned big endian |
||||
optional bytes gas_price = 3; // <=256 bit unsigned big endian (in wei) |
||||
optional bytes gas_limit = 4; // <=256 bit unsigned big endian |
||||
optional bytes to = 5; // 160 bit address hash |
||||
optional bytes value = 6; // <=256 bit unsigned big endian (in wei) |
||||
optional bytes data_initial_chunk = 7; // The initial data chunk (<= 1024 bytes) |
||||
optional uint32 data_length = 8; // Length of transaction payload |
||||
optional uint32 chain_id = 9; // Chain Id for EIP 155 |
||||
} |
||||
|
||||
/** |
||||
* Response: Device asks for more data from transaction payload, or returns the signature. |
||||
* If data_length is set, device awaits that many more bytes of payload. |
||||
* Otherwise, the signature_* fields contain the computed transaction signature. All three fields will be present. |
||||
* @prev EthereumSignTx |
||||
* @next EthereumTxAck |
||||
*/ |
||||
message EthereumTxRequest { |
||||
optional uint32 data_length = 1; // Number of bytes being requested (<= 1024) |
||||
optional uint32 signature_v = 2; // Computed signature (recovery parameter, limited to 27 or 28) |
||||
optional bytes signature_r = 3; // Computed signature R component (256 bit) |
||||
optional bytes signature_s = 4; // Computed signature S component (256 bit) |
||||
} |
||||
|
||||
/** |
||||
* Request: Transaction payload data. |
||||
* @prev EthereumTxRequest |
||||
* @next EthereumTxRequest |
||||
*/ |
||||
message EthereumTxAck { |
||||
optional bytes data_chunk = 1; // Bytes from transaction payload (<= 1024 bytes) |
||||
} |
||||
|
||||
//////////////////////////////////////// |
||||
// Ethereum: Message signing messages // |
||||
//////////////////////////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to sign message |
||||
* @next EthereumMessageSignature |
||||
* @next Failure |
||||
*/ |
||||
message EthereumSignMessage { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
required bytes message = 2; // message to be signed |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to verify message |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message EthereumVerifyMessage { |
||||
optional bytes address = 1; // address to verify |
||||
optional bytes signature = 2; // signature to verify |
||||
optional bytes message = 3; // message to verify |
||||
} |
||||
|
||||
/** |
||||
* Response: Signed message |
||||
* @prev EthereumSignMessage |
||||
*/ |
||||
message EthereumMessageSignature { |
||||
optional bytes address = 1; // address used to sign the message |
||||
optional bytes signature = 2; // signature of the message |
||||
} |
||||
|
||||
/////////////////////// |
||||
// Identity messages // |
||||
/////////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to sign identity |
||||
* @next SignedIdentity |
||||
* @next Failure |
||||
*/ |
||||
message SignIdentity { |
||||
optional IdentityType identity = 1; // identity |
||||
optional bytes challenge_hidden = 2; // non-visible challenge |
||||
optional string challenge_visual = 3; // challenge shown on display (e.g. date+time) |
||||
optional string ecdsa_curve_name = 4; // ECDSA curve name to use |
||||
} |
||||
|
||||
/** |
||||
* Response: Device provides signed identity |
||||
* @prev SignIdentity |
||||
*/ |
||||
message SignedIdentity { |
||||
optional string address = 1; // identity address |
||||
optional bytes public_key = 2; // identity public key |
||||
optional bytes signature = 3; // signature of the identity data |
||||
} |
||||
|
||||
/////////////////// |
||||
// ECDH messages // |
||||
/////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to generate ECDH session key |
||||
* @next ECDHSessionKey |
||||
* @next Failure |
||||
*/ |
||||
message GetECDHSessionKey { |
||||
optional IdentityType identity = 1; // identity |
||||
optional bytes peer_public_key = 2; // peer's public key |
||||
optional string ecdsa_curve_name = 3; // ECDSA curve name to use |
||||
} |
||||
|
||||
/** |
||||
* Response: Device provides ECDH session key |
||||
* @prev GetECDHSessionKey |
||||
*/ |
||||
message ECDHSessionKey { |
||||
optional bytes session_key = 1; // ECDH session key |
||||
} |
||||
|
||||
/////////////////// |
||||
// U2F messages // |
||||
/////////////////// |
||||
|
||||
/** |
||||
* Request: Set U2F counter |
||||
* @next Success |
||||
*/ |
||||
message SetU2FCounter { |
||||
optional uint32 u2f_counter = 1; // counter |
||||
} |
||||
|
||||
///////////////////////// |
||||
// Bootloader messages // |
||||
///////////////////////// |
||||
|
||||
/** |
||||
* Request: Ask device to erase its firmware (so it can be replaced via FirmwareUpload) |
||||
* @next Success |
||||
* @next FirmwareRequest |
||||
* @next Failure |
||||
*/ |
||||
message FirmwareErase { |
||||
optional uint32 length = 1; // length of new firmware |
||||
} |
||||
|
||||
/** |
||||
* Response: Ask for firmware chunk |
||||
* @next FirmwareUpload |
||||
*/ |
||||
message FirmwareRequest { |
||||
optional uint32 offset = 1; // offset of requested firmware chunk |
||||
optional uint32 length = 2; // length of requested firmware chunk |
||||
} |
||||
|
||||
/** |
||||
* Request: Send firmware in binary form to the device |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message FirmwareUpload { |
||||
required bytes payload = 1; // firmware to be loaded into device |
||||
optional bytes hash = 2; // hash of the payload |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Request: Perform a device self-test |
||||
* @next Success |
||||
* @next Failure |
||||
*/ |
||||
message SelfTest { |
||||
optional bytes payload = 1; // payload to be used in self-test |
||||
} |
||||
|
||||
///////////////////////////////////////////////////////////// |
||||
// Debug messages (only available if DebugLink is enabled) // |
||||
///////////////////////////////////////////////////////////// |
||||
|
||||
/** |
||||
* Request: "Press" the button on the device |
||||
* @next Success |
||||
*/ |
||||
message DebugLinkDecision { |
||||
required bool yes_no = 1; // true for "Confirm", false for "Cancel" |
||||
} |
||||
|
||||
/** |
||||
* Request: Computer asks for device state |
||||
* @next DebugLinkState |
||||
*/ |
||||
message DebugLinkGetState { |
||||
} |
||||
|
||||
/** |
||||
* Response: Device current state |
||||
* @prev DebugLinkGetState |
||||
*/ |
||||
message DebugLinkState { |
||||
optional bytes layout = 1; // raw buffer of display |
||||
optional string pin = 2; // current PIN, blank if PIN is not set/enabled |
||||
optional string matrix = 3; // current PIN matrix |
||||
optional string mnemonic = 4; // current BIP-39 mnemonic |
||||
optional HDNodeType node = 5; // current BIP-32 node |
||||
optional bool passphrase_protection = 6; // is node/mnemonic encrypted using passphrase? |
||||
optional string reset_word = 7; // word on device display during ResetDevice workflow |
||||
optional bytes reset_entropy = 8; // current entropy during ResetDevice workflow |
||||
optional string recovery_fake_word = 9; // (fake) word on display during RecoveryDevice workflow |
||||
optional uint32 recovery_word_pos = 10; // index of mnemonic word the device is expecting during RecoveryDevice workflow |
||||
} |
||||
|
||||
/** |
||||
* Request: Ask device to restart |
||||
*/ |
||||
message DebugLinkStop { |
||||
} |
||||
|
||||
/** |
||||
* Response: Device wants host to log event |
||||
*/ |
||||
message DebugLinkLog { |
||||
optional uint32 level = 1; |
||||
optional string bucket = 2; |
||||
optional string text = 3; |
||||
} |
||||
|
||||
/** |
||||
* Request: Read memory from device |
||||
* @next DebugLinkMemory |
||||
*/ |
||||
message DebugLinkMemoryRead { |
||||
optional uint32 address = 1; |
||||
optional uint32 length = 2; |
||||
} |
||||
|
||||
/** |
||||
* Response: Device sends memory back |
||||
* @prev DebugLinkMemoryRead |
||||
*/ |
||||
message DebugLinkMemory { |
||||
optional bytes memory = 1; |
||||
} |
||||
|
||||
/** |
||||
* Request: Write memory to device. |
||||
* WARNING: Writing to the wrong location can irreparably break the device. |
||||
*/ |
||||
message DebugLinkMemoryWrite { |
||||
optional uint32 address = 1; |
||||
optional bytes memory = 2; |
||||
optional bool flash = 3; |
||||
} |
||||
|
||||
/** |
||||
* Request: Erase block of flash on device |
||||
* WARNING: Writing to the wrong location can irreparably break the device. |
||||
*/ |
||||
message DebugLinkFlashErase { |
||||
optional uint32 sector = 1; |
||||
} |
@ -0,0 +1,46 @@ |
||||
// 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/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Trezor hardware
|
||||
// wallets. The wire protocol spec can be found on the SatoshiLabs website:
|
||||
// https://doc.satoshilabs.com/trezor-tech/api-protobuf.html
|
||||
|
||||
//go:generate protoc --go_out=import_path=trezor:. types.proto messages.proto
|
||||
|
||||
// Package trezor contains the wire protocol wrapper in Go.
|
||||
package trezor |
||||
|
||||
import ( |
||||
"reflect" |
||||
|
||||
"github.com/golang/protobuf/proto" |
||||
) |
||||
|
||||
// Type returns the protocol buffer type number of a specific message. If the
|
||||
// message is nil, this method panics!
|
||||
func Type(msg proto.Message) uint16 { |
||||
return uint16(MessageType_value["MessageType_"+reflect.TypeOf(msg).Elem().Name()]) |
||||
} |
||||
|
||||
// Name returns the friendly message type name of a specific protocol buffer
|
||||
// type number.
|
||||
func Name(kind uint16) string { |
||||
name := MessageType_name[int32(kind)] |
||||
if len(name) < 12 { |
||||
return name |
||||
} |
||||
return name[12:] |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,278 @@ |
||||
// This file originates from the SatoshiLabs Trezor `common` repository at: |
||||
// https://github.com/trezor/trezor-common/blob/master/protob/types.proto |
||||
// dated 28.07.2017, commit dd8ec3231fb5f7992360aff9bdfe30bb58130f4b. |
||||
|
||||
syntax = "proto2"; |
||||
|
||||
/** |
||||
* Types for TREZOR communication |
||||
* |
||||
* @author Marek Palatinus <slush@satoshilabs.com> |
||||
* @version 1.2 |
||||
*/ |
||||
|
||||
// Sugar for easier handling in Java |
||||
option java_package = "com.satoshilabs.trezor.lib.protobuf"; |
||||
option java_outer_classname = "TrezorType"; |
||||
|
||||
import "google/protobuf/descriptor.proto"; |
||||
|
||||
/** |
||||
* Options for specifying message direction and type of wire (normal/debug) |
||||
*/ |
||||
extend google.protobuf.EnumValueOptions { |
||||
optional bool wire_in = 50002; // message can be transmitted via wire from PC to TREZOR |
||||
optional bool wire_out = 50003; // message can be transmitted via wire from TREZOR to PC |
||||
optional bool wire_debug_in = 50004; // message can be transmitted via debug wire from PC to TREZOR |
||||
optional bool wire_debug_out = 50005; // message can be transmitted via debug wire from TREZOR to PC |
||||
optional bool wire_tiny = 50006; // message is handled by TREZOR when the USB stack is in tiny mode |
||||
optional bool wire_bootloader = 50007; // message is only handled by TREZOR Bootloader |
||||
} |
||||
|
||||
/** |
||||
* Type of failures returned by Failure message |
||||
* @used_in Failure |
||||
*/ |
||||
enum FailureType { |
||||
Failure_UnexpectedMessage = 1; |
||||
Failure_ButtonExpected = 2; |
||||
Failure_DataError = 3; |
||||
Failure_ActionCancelled = 4; |
||||
Failure_PinExpected = 5; |
||||
Failure_PinCancelled = 6; |
||||
Failure_PinInvalid = 7; |
||||
Failure_InvalidSignature = 8; |
||||
Failure_ProcessError = 9; |
||||
Failure_NotEnoughFunds = 10; |
||||
Failure_NotInitialized = 11; |
||||
Failure_FirmwareError = 99; |
||||
} |
||||
|
||||
/** |
||||
* Type of script which will be used for transaction output |
||||
* @used_in TxOutputType |
||||
*/ |
||||
enum OutputScriptType { |
||||
PAYTOADDRESS = 0; // used for all addresses (bitcoin, p2sh, witness) |
||||
PAYTOSCRIPTHASH = 1; // p2sh address (deprecated; use PAYTOADDRESS) |
||||
PAYTOMULTISIG = 2; // only for change output |
||||
PAYTOOPRETURN = 3; // op_return |
||||
PAYTOWITNESS = 4; // only for change output |
||||
PAYTOP2SHWITNESS = 5; // only for change output |
||||
} |
||||
|
||||
/** |
||||
* Type of script which will be used for transaction output |
||||
* @used_in TxInputType |
||||
*/ |
||||
enum InputScriptType { |
||||
SPENDADDRESS = 0; // standard p2pkh address |
||||
SPENDMULTISIG = 1; // p2sh multisig address |
||||
EXTERNAL = 2; // reserved for external inputs (coinjoin) |
||||
SPENDWITNESS = 3; // native segwit |
||||
SPENDP2SHWITNESS = 4; // segwit over p2sh (backward compatible) |
||||
} |
||||
|
||||
/** |
||||
* Type of information required by transaction signing process |
||||
* @used_in TxRequest |
||||
*/ |
||||
enum RequestType { |
||||
TXINPUT = 0; |
||||
TXOUTPUT = 1; |
||||
TXMETA = 2; |
||||
TXFINISHED = 3; |
||||
TXEXTRADATA = 4; |
||||
} |
||||
|
||||
/** |
||||
* Type of button request |
||||
* @used_in ButtonRequest |
||||
*/ |
||||
enum ButtonRequestType { |
||||
ButtonRequest_Other = 1; |
||||
ButtonRequest_FeeOverThreshold = 2; |
||||
ButtonRequest_ConfirmOutput = 3; |
||||
ButtonRequest_ResetDevice = 4; |
||||
ButtonRequest_ConfirmWord = 5; |
||||
ButtonRequest_WipeDevice = 6; |
||||
ButtonRequest_ProtectCall = 7; |
||||
ButtonRequest_SignTx = 8; |
||||
ButtonRequest_FirmwareCheck = 9; |
||||
ButtonRequest_Address = 10; |
||||
ButtonRequest_PublicKey = 11; |
||||
} |
||||
|
||||
/** |
||||
* Type of PIN request |
||||
* @used_in PinMatrixRequest |
||||
*/ |
||||
enum PinMatrixRequestType { |
||||
PinMatrixRequestType_Current = 1; |
||||
PinMatrixRequestType_NewFirst = 2; |
||||
PinMatrixRequestType_NewSecond = 3; |
||||
} |
||||
|
||||
/** |
||||
* Type of recovery procedure. These should be used as bitmask, e.g., |
||||
* `RecoveryDeviceType_ScrambledWords | RecoveryDeviceType_Matrix` |
||||
* listing every method supported by the host computer. |
||||
* |
||||
* Note that ScrambledWords must be supported by every implementation |
||||
* for backward compatibility; there is no way to not support it. |
||||
* |
||||
* @used_in RecoveryDevice |
||||
*/ |
||||
enum RecoveryDeviceType { |
||||
// use powers of two when extending this field |
||||
RecoveryDeviceType_ScrambledWords = 0; // words in scrambled order |
||||
RecoveryDeviceType_Matrix = 1; // matrix recovery type |
||||
} |
||||
|
||||
/** |
||||
* Type of Recovery Word request |
||||
* @used_in WordRequest |
||||
*/ |
||||
enum WordRequestType { |
||||
WordRequestType_Plain = 0; |
||||
WordRequestType_Matrix9 = 1; |
||||
WordRequestType_Matrix6 = 2; |
||||
} |
||||
|
||||
/** |
||||
* Structure representing BIP32 (hierarchical deterministic) node |
||||
* Used for imports of private key into the device and exporting public key out of device |
||||
* @used_in PublicKey |
||||
* @used_in LoadDevice |
||||
* @used_in DebugLinkState |
||||
* @used_in Storage |
||||
*/ |
||||
message HDNodeType { |
||||
required uint32 depth = 1; |
||||
required uint32 fingerprint = 2; |
||||
required uint32 child_num = 3; |
||||
required bytes chain_code = 4; |
||||
optional bytes private_key = 5; |
||||
optional bytes public_key = 6; |
||||
} |
||||
|
||||
message HDNodePathType { |
||||
required HDNodeType node = 1; // BIP-32 node in deserialized form |
||||
repeated uint32 address_n = 2; // BIP-32 path to derive the key from node |
||||
} |
||||
|
||||
/** |
||||
* Structure representing Coin |
||||
* @used_in Features |
||||
*/ |
||||
message CoinType { |
||||
optional string coin_name = 1; |
||||
optional string coin_shortcut = 2; |
||||
optional uint32 address_type = 3 [default=0]; |
||||
optional uint64 maxfee_kb = 4; |
||||
optional uint32 address_type_p2sh = 5 [default=5]; |
||||
optional string signed_message_header = 8; |
||||
optional uint32 xpub_magic = 9 [default=76067358]; // default=0x0488b21e |
||||
optional uint32 xprv_magic = 10 [default=76066276]; // default=0x0488ade4 |
||||
optional bool segwit = 11; |
||||
optional uint32 forkid = 12; |
||||
} |
||||
|
||||
/** |
||||
* Type of redeem script used in input |
||||
* @used_in TxInputType |
||||
*/ |
||||
message MultisigRedeemScriptType { |
||||
repeated HDNodePathType pubkeys = 1; // pubkeys from multisig address (sorted lexicographically) |
||||
repeated bytes signatures = 2; // existing signatures for partially signed input |
||||
optional uint32 m = 3; // "m" from n, how many valid signatures is necessary for spending |
||||
} |
||||
|
||||
/** |
||||
* Structure representing transaction input |
||||
* @used_in SimpleSignTx |
||||
* @used_in TransactionType |
||||
*/ |
||||
message TxInputType { |
||||
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node |
||||
required bytes prev_hash = 2; // hash of previous transaction output to spend by this input |
||||
required uint32 prev_index = 3; // index of previous output to spend |
||||
optional bytes script_sig = 4; // script signature, unset for tx to sign |
||||
optional uint32 sequence = 5 [default=4294967295]; // sequence (default=0xffffffff) |
||||
optional InputScriptType script_type = 6 [default=SPENDADDRESS]; // defines template of input script |
||||
optional MultisigRedeemScriptType multisig = 7; // Filled if input is going to spend multisig tx |
||||
optional uint64 amount = 8; // amount of previous transaction output (for segwit only) |
||||
} |
||||
|
||||
/** |
||||
* Structure representing transaction output |
||||
* @used_in SimpleSignTx |
||||
* @used_in TransactionType |
||||
*/ |
||||
message TxOutputType { |
||||
optional string address = 1; // target coin address in Base58 encoding |
||||
repeated uint32 address_n = 2; // BIP-32 path to derive the key from master node; has higher priority than "address" |
||||
required uint64 amount = 3; // amount to spend in satoshis |
||||
required OutputScriptType script_type = 4; // output script type |
||||
optional MultisigRedeemScriptType multisig = 5; // defines multisig address; script_type must be PAYTOMULTISIG |
||||
optional bytes op_return_data = 6; // defines op_return data; script_type must be PAYTOOPRETURN, amount must be 0 |
||||
} |
||||
|
||||
/** |
||||
* Structure representing compiled transaction output |
||||
* @used_in TransactionType |
||||
*/ |
||||
message TxOutputBinType { |
||||
required uint64 amount = 1; |
||||
required bytes script_pubkey = 2; |
||||
} |
||||
|
||||
/** |
||||
* Structure representing transaction |
||||
* @used_in SimpleSignTx |
||||
*/ |
||||
message TransactionType { |
||||
optional uint32 version = 1; |
||||
repeated TxInputType inputs = 2; |
||||
repeated TxOutputBinType bin_outputs = 3; |
||||
repeated TxOutputType outputs = 5; |
||||
optional uint32 lock_time = 4; |
||||
optional uint32 inputs_cnt = 6; |
||||
optional uint32 outputs_cnt = 7; |
||||
optional bytes extra_data = 8; |
||||
optional uint32 extra_data_len = 9; |
||||
} |
||||
|
||||
/** |
||||
* Structure representing request details |
||||
* @used_in TxRequest |
||||
*/ |
||||
message TxRequestDetailsType { |
||||
optional uint32 request_index = 1; // device expects TxAck message from the computer |
||||
optional bytes tx_hash = 2; // tx_hash of requested transaction |
||||
optional uint32 extra_data_len = 3; // length of requested extra data |
||||
optional uint32 extra_data_offset = 4; // offset of requested extra data |
||||
} |
||||
|
||||
/** |
||||
* Structure representing serialized data |
||||
* @used_in TxRequest |
||||
*/ |
||||
message TxRequestSerializedType { |
||||
optional uint32 signature_index = 1; // 'signature' field contains signed input of this index |
||||
optional bytes signature = 2; // signature of the signature_index input |
||||
optional bytes serialized_tx = 3; // part of serialized and signed transaction |
||||
} |
||||
|
||||
/** |
||||
* Structure representing identity data |
||||
* @used_in IdentityType |
||||
*/ |
||||
message IdentityType { |
||||
optional string proto = 1; // proto part of URI |
||||
optional string user = 2; // user part of URI |
||||
optional string host = 3; // host part of URI |
||||
optional string port = 4; // port part of URI |
||||
optional string path = 5; // path part of URI |
||||
optional uint32 index = 6 [default=0]; // identity index |
||||
} |
@ -0,0 +1,594 @@ |
||||
// 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 usbwallet implements support for USB hardware wallets.
|
||||
package usbwallet |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
"sync" |
||||
"time" |
||||
|
||||
ethereum "github.com/ethereum/go-ethereum" |
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/karalabe/hid" |
||||
) |
||||
|
||||
// Maximum time between wallet health checks to detect USB unplugs.
|
||||
const heartbeatCycle = time.Second |
||||
|
||||
// Minimum time to wait between self derivation attempts, even it the user is
|
||||
// requesting accounts like crazy.
|
||||
const selfDeriveThrottling = time.Second |
||||
|
||||
// driver defines the vendor specific functionality hardware wallets instances
|
||||
// must implement to allow using them with the wallet lifecycle management.
|
||||
type driver interface { |
||||
// Status returns a textual status to aid the user in the current state of the
|
||||
// wallet. It also returns an error indicating any failure the wallet might have
|
||||
// encountered.
|
||||
Status() (string, error) |
||||
|
||||
// Open initializes access to a wallet instance. The passphrase parameter may
|
||||
// or may not be used by the implementation of a particular wallet instance.
|
||||
Open(device io.ReadWriter, passphrase string) error |
||||
|
||||
// Close releases any resources held by an open wallet instance.
|
||||
Close() error |
||||
|
||||
// Heartbeat performs a sanity check against the hardware wallet to see if it
|
||||
// is still online and healthy.
|
||||
Heartbeat() error |
||||
|
||||
// Derive sends a derivation request to the USB device and returns the Ethereum
|
||||
// address located on that path.
|
||||
Derive(path accounts.DerivationPath) (common.Address, error) |
||||
|
||||
// SignTx sends the transaction to the USB device and waits for the user to confirm
|
||||
// or deny the transaction.
|
||||
SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) |
||||
} |
||||
|
||||
// wallet represents the common functionality shared by all USB hardware
|
||||
// wallets to prevent reimplementing the same complex maintenance mechanisms
|
||||
// for different vendors.
|
||||
type wallet struct { |
||||
hub *Hub // USB hub scanning
|
||||
driver driver // Hardware implementation of the low level device operations
|
||||
url *accounts.URL // Textual URL uniquely identifying this wallet
|
||||
|
||||
info hid.DeviceInfo // Known USB device infos about the wallet
|
||||
device *hid.Device // USB device advertising itself as a hardware wallet
|
||||
|
||||
accounts []accounts.Account // List of derive accounts pinned on the hardware wallet
|
||||
paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
|
||||
|
||||
deriveNextPaths []accounts.DerivationPath // Next derivation paths for account auto-discovery (multiple bases supported)
|
||||
deriveNextAddrs []common.Address // Next derived account addresses for auto-discovery (multiple bases supported)
|
||||
deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
|
||||
deriveReq chan chan struct{} // Channel to request a self-derivation on
|
||||
deriveQuit chan chan error // Channel to terminate the self-deriver with
|
||||
|
||||
healthQuit chan chan error |
||||
|
||||
// Locking a hardware wallet is a bit special. Since hardware devices are lower
|
||||
// performing, any communication with them might take a non negligible amount of
|
||||
// time. Worse still, waiting for user confirmation can take arbitrarily long,
|
||||
// but exclusive communication must be upheld during. Locking the entire wallet
|
||||
// in the mean time however would stall any parts of the system that don't want
|
||||
// to communicate, just read some state (e.g. list the accounts).
|
||||
//
|
||||
// As such, a hardware wallet needs two locks to function correctly. A state
|
||||
// lock can be used to protect the wallet's software-side internal state, which
|
||||
// must not be held exclusively during hardware communication. A communication
|
||||
// lock can be used to achieve exclusive access to the device itself, this one
|
||||
// however should allow "skipping" waiting for operations that might want to
|
||||
// use the device, but can live without too (e.g. account self-derivation).
|
||||
//
|
||||
// Since we have two locks, it's important to know how to properly use them:
|
||||
// - Communication requires the `device` to not change, so obtaining the
|
||||
// commsLock should be done after having a stateLock.
|
||||
// - Communication must not disable read access to the wallet state, so it
|
||||
// must only ever hold a *read* lock to stateLock.
|
||||
commsLock chan struct{} // Mutex (buf=1) for the USB comms without keeping the state locked
|
||||
stateLock sync.RWMutex // Protects read and write access to the wallet struct fields
|
||||
|
||||
log log.Logger // Contextual logger to tag the base with its id
|
||||
} |
||||
|
||||
// URL implements accounts.Wallet, returning the URL of the USB hardware device.
|
||||
func (w *wallet) URL() accounts.URL { |
||||
return *w.url // Immutable, no need for a lock
|
||||
} |
||||
|
||||
// Status implements accounts.Wallet, returning a custom status message from the
|
||||
// underlying vendor-specific hardware wallet implementation.
|
||||
func (w *wallet) Status() (string, error) { |
||||
w.stateLock.RLock() // No device communication, state lock is enough
|
||||
defer w.stateLock.RUnlock() |
||||
|
||||
status, failure := w.driver.Status() |
||||
if w.device == nil { |
||||
return "Closed", failure |
||||
} |
||||
return status, failure |
||||
} |
||||
|
||||
// Open implements accounts.Wallet, attempting to open a USB connection to the
|
||||
// hardware wallet.
|
||||
func (w *wallet) Open(passphrase string) error { |
||||
w.stateLock.Lock() // State lock is enough since there's no connection yet at this point
|
||||
defer w.stateLock.Unlock() |
||||
|
||||
// If the device was already opened once, refuse to try again
|
||||
if w.paths != nil { |
||||
return accounts.ErrWalletAlreadyOpen |
||||
} |
||||
// Make sure the actual device connection is done only once
|
||||
if w.device == nil { |
||||
device, err := w.info.Open() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
w.device = device |
||||
w.commsLock = make(chan struct{}, 1) |
||||
w.commsLock <- struct{}{} // Enable lock
|
||||
} |
||||
// Delegate device initialization to the underlying driver
|
||||
if err := w.driver.Open(w.device, passphrase); err != nil { |
||||
return err |
||||
} |
||||
// Connection successful, start life-cycle management
|
||||
w.paths = make(map[common.Address]accounts.DerivationPath) |
||||
|
||||
w.deriveReq = make(chan chan struct{}) |
||||
w.deriveQuit = make(chan chan error) |
||||
w.healthQuit = make(chan chan error) |
||||
|
||||
go w.heartbeat() |
||||
go w.selfDerive() |
||||
|
||||
// Notify anyone listening for wallet events that a new device is accessible
|
||||
go w.hub.updateFeed.Send(accounts.WalletEvent{Wallet: w, Kind: accounts.WalletOpened}) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// heartbeat is a health check loop for the USB wallets to periodically verify
|
||||
// whether they are still present or if they malfunctioned.
|
||||
func (w *wallet) heartbeat() { |
||||
w.log.Debug("USB wallet health-check started") |
||||
defer w.log.Debug("USB wallet health-check stopped") |
||||
|
||||
// Execute heartbeat checks until termination or error
|
||||
var ( |
||||
errc chan error |
||||
err error |
||||
) |
||||
for errc == nil && err == nil { |
||||
// Wait until termination is requested or the heartbeat cycle arrives
|
||||
select { |
||||
case errc = <-w.healthQuit: |
||||
// Termination requested
|
||||
continue |
||||
case <-time.After(heartbeatCycle): |
||||
// Heartbeat time
|
||||
} |
||||
// Execute a tiny data exchange to see responsiveness
|
||||
w.stateLock.RLock() |
||||
if w.device == nil { |
||||
// Terminated while waiting for the lock
|
||||
w.stateLock.RUnlock() |
||||
continue |
||||
} |
||||
<-w.commsLock // Don't lock state while resolving version
|
||||
err = w.driver.Heartbeat() |
||||
w.commsLock <- struct{}{} |
||||
w.stateLock.RUnlock() |
||||
|
||||
if err != nil { |
||||
w.stateLock.Lock() // Lock state to tear the wallet down
|
||||
w.close() |
||||
w.stateLock.Unlock() |
||||
} |
||||
// Ignore non hardware related errors
|
||||
err = nil |
||||
} |
||||
// In case of error, wait for termination
|
||||
if err != nil { |
||||
w.log.Debug("USB wallet health-check failed", "err", err) |
||||
errc = <-w.healthQuit |
||||
} |
||||
errc <- err |
||||
} |
||||
|
||||
// Close implements accounts.Wallet, closing the USB connection to the device.
|
||||
func (w *wallet) Close() error { |
||||
// Ensure the wallet was opened
|
||||
w.stateLock.RLock() |
||||
hQuit, dQuit := w.healthQuit, w.deriveQuit |
||||
w.stateLock.RUnlock() |
||||
|
||||
// Terminate the health checks
|
||||
var herr error |
||||
if hQuit != nil { |
||||
errc := make(chan error) |
||||
hQuit <- errc |
||||
herr = <-errc // Save for later, we *must* close the USB
|
||||
} |
||||
// Terminate the self-derivations
|
||||
var derr error |
||||
if dQuit != nil { |
||||
errc := make(chan error) |
||||
dQuit <- errc |
||||
derr = <-errc // Save for later, we *must* close the USB
|
||||
} |
||||
// Terminate the device connection
|
||||
w.stateLock.Lock() |
||||
defer w.stateLock.Unlock() |
||||
|
||||
w.healthQuit = nil |
||||
w.deriveQuit = nil |
||||
w.deriveReq = nil |
||||
|
||||
if err := w.close(); err != nil { |
||||
return err |
||||
} |
||||
if herr != nil { |
||||
return herr |
||||
} |
||||
return derr |
||||
} |
||||
|
||||
// close is the internal wallet closer that terminates the USB connection and
|
||||
// resets all the fields to their defaults.
|
||||
//
|
||||
// Note, close assumes the state lock is held!
|
||||
func (w *wallet) close() error { |
||||
// Allow duplicate closes, especially for health-check failures
|
||||
if w.device == nil { |
||||
return nil |
||||
} |
||||
// Close the device, clear everything, then return
|
||||
w.device.Close() |
||||
w.device = nil |
||||
|
||||
w.accounts, w.paths = nil, nil |
||||
return w.driver.Close() |
||||
} |
||||
|
||||
// Accounts implements accounts.Wallet, returning the list of accounts pinned to
|
||||
// the USB hardware wallet. If self-derivation was enabled, the account list is
|
||||
// periodically expanded based on current chain state.
|
||||
func (w *wallet) Accounts() []accounts.Account { |
||||
// Attempt self-derivation if it's running
|
||||
reqc := make(chan struct{}, 1) |
||||
select { |
||||
case w.deriveReq <- reqc: |
||||
// Self-derivation request accepted, wait for it
|
||||
<-reqc |
||||
default: |
||||
// Self-derivation offline, throttled or busy, skip
|
||||
} |
||||
// Return whatever account list we ended up with
|
||||
w.stateLock.RLock() |
||||
defer w.stateLock.RUnlock() |
||||
|
||||
cpy := make([]accounts.Account, len(w.accounts)) |
||||
copy(cpy, w.accounts) |
||||
return cpy |
||||
} |
||||
|
||||
// selfDerive is an account derivation loop that upon request attempts to find
|
||||
// new non-zero accounts.
|
||||
func (w *wallet) selfDerive() { |
||||
w.log.Debug("USB wallet self-derivation started") |
||||
defer w.log.Debug("USB wallet self-derivation stopped") |
||||
|
||||
// Execute self-derivations until termination or error
|
||||
var ( |
||||
reqc chan struct{} |
||||
errc chan error |
||||
err error |
||||
) |
||||
for errc == nil && err == nil { |
||||
// Wait until either derivation or termination is requested
|
||||
select { |
||||
case errc = <-w.deriveQuit: |
||||
// Termination requested
|
||||
continue |
||||
case reqc = <-w.deriveReq: |
||||
// Account discovery requested
|
||||
} |
||||
// Derivation needs a chain and device access, skip if either unavailable
|
||||
w.stateLock.RLock() |
||||
if w.device == nil || w.deriveChain == nil { |
||||
w.stateLock.RUnlock() |
||||
reqc <- struct{}{} |
||||
continue |
||||
} |
||||
select { |
||||
case <-w.commsLock: |
||||
default: |
||||
w.stateLock.RUnlock() |
||||
reqc <- struct{}{} |
||||
continue |
||||
} |
||||
// Device lock obtained, derive the next batch of accounts
|
||||
var ( |
||||
accs []accounts.Account |
||||
paths []accounts.DerivationPath |
||||
|
||||
nextPaths = append([]accounts.DerivationPath{}, w.deriveNextPaths...) |
||||
nextAddrs = append([]common.Address{}, w.deriveNextAddrs...) |
||||
|
||||
context = context.Background() |
||||
) |
||||
for i := 0; i < len(nextAddrs); i++ { |
||||
for empty := false; !empty; { |
||||
// Retrieve the next derived Ethereum account
|
||||
if nextAddrs[i] == (common.Address{}) { |
||||
if nextAddrs[i], err = w.driver.Derive(nextPaths[i]); err != nil { |
||||
w.log.Warn("USB wallet account derivation failed", "err", err) |
||||
break |
||||
} |
||||
} |
||||
// Check the account's status against the current chain state
|
||||
var ( |
||||
balance *big.Int |
||||
nonce uint64 |
||||
) |
||||
balance, err = w.deriveChain.BalanceAt(context, nextAddrs[i], nil) |
||||
if err != nil { |
||||
w.log.Warn("USB wallet balance retrieval failed", "err", err) |
||||
break |
||||
} |
||||
nonce, err = w.deriveChain.NonceAt(context, nextAddrs[i], nil) |
||||
if err != nil { |
||||
w.log.Warn("USB wallet nonce retrieval failed", "err", err) |
||||
break |
||||
} |
||||
// If the next account is empty, stop self-derivation, but add for the last base path
|
||||
if balance.Sign() == 0 && nonce == 0 { |
||||
empty = true |
||||
if i < len(nextAddrs)-1 { |
||||
break |
||||
} |
||||
} |
||||
// We've just self-derived a new account, start tracking it locally
|
||||
path := make(accounts.DerivationPath, len(nextPaths[i])) |
||||
copy(path[:], nextPaths[i][:]) |
||||
paths = append(paths, path) |
||||
|
||||
account := accounts.Account{ |
||||
Address: nextAddrs[i], |
||||
URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, |
||||
} |
||||
accs = append(accs, account) |
||||
|
||||
// Display a log message to the user for new (or previously empty accounts)
|
||||
if _, known := w.paths[nextAddrs[i]]; !known || (!empty && nextAddrs[i] == w.deriveNextAddrs[i]) { |
||||
w.log.Info("USB wallet discovered new account", "address", nextAddrs[i], "path", path, "balance", balance, "nonce", nonce) |
||||
} |
||||
// Fetch the next potential account
|
||||
if !empty { |
||||
nextAddrs[i] = common.Address{} |
||||
nextPaths[i][len(nextPaths[i])-1]++ |
||||
} |
||||
} |
||||
} |
||||
// Self derivation complete, release device lock
|
||||
w.commsLock <- struct{}{} |
||||
w.stateLock.RUnlock() |
||||
|
||||
// Insert any accounts successfully derived
|
||||
w.stateLock.Lock() |
||||
for i := 0; i < len(accs); i++ { |
||||
if _, ok := w.paths[accs[i].Address]; !ok { |
||||
w.accounts = append(w.accounts, accs[i]) |
||||
w.paths[accs[i].Address] = paths[i] |
||||
} |
||||
} |
||||
// Shift the self-derivation forward
|
||||
// TODO(karalabe): don't overwrite changes from wallet.SelfDerive
|
||||
w.deriveNextAddrs = nextAddrs |
||||
w.deriveNextPaths = nextPaths |
||||
w.stateLock.Unlock() |
||||
|
||||
// Notify the user of termination and loop after a bit of time (to avoid trashing)
|
||||
reqc <- struct{}{} |
||||
if err == nil { |
||||
select { |
||||
case errc = <-w.deriveQuit: |
||||
// Termination requested, abort
|
||||
case <-time.After(selfDeriveThrottling): |
||||
// Waited enough, willing to self-derive again
|
||||
} |
||||
} |
||||
} |
||||
// In case of error, wait for termination
|
||||
if err != nil { |
||||
w.log.Debug("USB wallet self-derivation failed", "err", err) |
||||
errc = <-w.deriveQuit |
||||
} |
||||
errc <- err |
||||
} |
||||
|
||||
// Contains implements accounts.Wallet, returning whether a particular account is
|
||||
// or is not pinned into this wallet instance. Although we could attempt to resolve
|
||||
// unpinned accounts, that would be an non-negligible hardware operation.
|
||||
func (w *wallet) Contains(account accounts.Account) bool { |
||||
w.stateLock.RLock() |
||||
defer w.stateLock.RUnlock() |
||||
|
||||
_, exists := w.paths[account.Address] |
||||
return exists |
||||
} |
||||
|
||||
// Derive implements accounts.Wallet, deriving a new account at the specific
|
||||
// derivation path. If pin is set to true, the account will be added to the list
|
||||
// of tracked accounts.
|
||||
func (w *wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { |
||||
// Try to derive the actual account and update its URL if successful
|
||||
w.stateLock.RLock() // Avoid device disappearing during derivation
|
||||
|
||||
if w.device == nil { |
||||
w.stateLock.RUnlock() |
||||
return accounts.Account{}, accounts.ErrWalletClosed |
||||
} |
||||
<-w.commsLock // Avoid concurrent hardware access
|
||||
address, err := w.driver.Derive(path) |
||||
w.commsLock <- struct{}{} |
||||
|
||||
w.stateLock.RUnlock() |
||||
|
||||
// If an error occurred or no pinning was requested, return
|
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
account := accounts.Account{ |
||||
Address: address, |
||||
URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, |
||||
} |
||||
if !pin { |
||||
return account, nil |
||||
} |
||||
// Pinning needs to modify the state
|
||||
w.stateLock.Lock() |
||||
defer w.stateLock.Unlock() |
||||
|
||||
if _, ok := w.paths[address]; !ok { |
||||
w.accounts = append(w.accounts, account) |
||||
w.paths[address] = path |
||||
} |
||||
return account, nil |
||||
} |
||||
|
||||
// SelfDerive sets a base account derivation path from which the wallet attempts
|
||||
// to discover non zero accounts and automatically add them to list of tracked
|
||||
// accounts.
|
||||
//
|
||||
// Note, self derivaton will increment the last component of the specified path
|
||||
// opposed to decending into a child path to allow discovering accounts starting
|
||||
// from non zero components.
|
||||
//
|
||||
// Some hardware wallets switched derivation paths through their evolution, so
|
||||
// this method supports providing multiple bases to discover old user accounts
|
||||
// too. Only the last base will be used to derive the next empty account.
|
||||
//
|
||||
// You can disable automatic account discovery by calling SelfDerive with a nil
|
||||
// chain state reader.
|
||||
func (w *wallet) SelfDerive(bases []accounts.DerivationPath, chain ethereum.ChainStateReader) { |
||||
w.stateLock.Lock() |
||||
defer w.stateLock.Unlock() |
||||
|
||||
w.deriveNextPaths = make([]accounts.DerivationPath, len(bases)) |
||||
for i, base := range bases { |
||||
w.deriveNextPaths[i] = make(accounts.DerivationPath, len(base)) |
||||
copy(w.deriveNextPaths[i][:], base[:]) |
||||
} |
||||
w.deriveNextAddrs = make([]common.Address, len(bases)) |
||||
w.deriveChain = chain |
||||
} |
||||
|
||||
// signHash implements accounts.Wallet, however signing arbitrary data is not
|
||||
// supported for hardware wallets, so this method will always return an error.
|
||||
func (w *wallet) signHash(account accounts.Account, hash []byte) ([]byte, error) { |
||||
return nil, accounts.ErrNotSupported |
||||
} |
||||
|
||||
// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
|
||||
func (w *wallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { |
||||
return w.signHash(account, crypto.Keccak256(data)) |
||||
} |
||||
|
||||
// SignDataWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||
// data with the given account using passphrase as extra authentication.
|
||||
// Since USB wallets don't rely on passphrases, these are silently ignored.
|
||||
func (w *wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { |
||||
return w.SignData(account, mimeType, data) |
||||
} |
||||
|
||||
func (w *wallet) SignText(account accounts.Account, text []byte) ([]byte, error) { |
||||
return w.signHash(account, accounts.TextHash(text)) |
||||
} |
||||
|
||||
// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger
|
||||
// wallet to request a confirmation from the user. It returns either the signed
|
||||
// transaction or a failure if the user denied the transaction.
|
||||
//
|
||||
// Note, if the version of the Ethereum application running on the Ledger wallet is
|
||||
// too old to sign EIP-155 transactions, but such is requested nonetheless, an error
|
||||
// will be returned opposed to silently signing in Homestead mode.
|
||||
func (w *wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
w.stateLock.RLock() // Comms have own mutex, this is for the state fields
|
||||
defer w.stateLock.RUnlock() |
||||
|
||||
// If the wallet is closed, abort
|
||||
if w.device == nil { |
||||
return nil, accounts.ErrWalletClosed |
||||
} |
||||
// Make sure the requested account is contained within
|
||||
path, ok := w.paths[account.Address] |
||||
if !ok { |
||||
return nil, accounts.ErrUnknownAccount |
||||
} |
||||
// All infos gathered and metadata checks out, request signing
|
||||
<-w.commsLock |
||||
defer func() { w.commsLock <- struct{}{} }() |
||||
|
||||
// Ensure the device isn't screwed with while user confirmation is pending
|
||||
// TODO(karalabe): remove if hotplug lands on Windows
|
||||
w.hub.commsLock.Lock() |
||||
w.hub.commsPend++ |
||||
w.hub.commsLock.Unlock() |
||||
|
||||
defer func() { |
||||
w.hub.commsLock.Lock() |
||||
w.hub.commsPend-- |
||||
w.hub.commsLock.Unlock() |
||||
}() |
||||
// Sign the transaction and verify the sender to avoid hardware fault surprises
|
||||
sender, signed, err := w.driver.SignTx(path, tx, chainID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if sender != account.Address { |
||||
return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex()) |
||||
} |
||||
return signed, nil |
||||
} |
||||
|
||||
// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary
|
||||
// data is not supported for Ledger wallets, so this method will always return
|
||||
// an error.
|
||||
func (w *wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { |
||||
return w.SignText(account, accounts.TextHash(text)) |
||||
} |
||||
|
||||
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||
// transaction with the given account using passphrase as extra authentication.
|
||||
// Since USB wallets don't rely on passphrases, these are silently ignored.
|
||||
func (w *wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||
return w.SignTx(account, tx, chainID) |
||||
} |
Loading…
Reference in new issue