Initial commit
This commit is contained in:
commit
7753247afb
1
MANIFEST.in
Normal file
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
include *requirements*
|
3
config/config.ini
Normal file
3
config/config.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[signer]
|
||||||
|
secret = deadbeef
|
||||||
|
socket_path = ipc:///tmp/crypto-dev-signer/jsonrpc.ipc
|
6
config/database.ini
Normal file
6
config/database.ini
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[database]
|
||||||
|
NAME=cic-signer
|
||||||
|
USER=postgres
|
||||||
|
PASSWORD=
|
||||||
|
HOST=localhost
|
||||||
|
PORT=5432
|
0
funga/eth/__init__.py
Normal file
0
funga/eth/__init__.py
Normal file
0
funga/eth/cli/__init__.py
Normal file
0
funga/eth/cli/__init__.py
Normal file
115
funga/eth/cli/handle.py
Normal file
115
funga/eth/cli/handle.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# standard imports
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from jsonrpc.exceptions import (
|
||||||
|
JSONRPCServerError,
|
||||||
|
JSONRPCParseError,
|
||||||
|
JSONRPCInvalidParams,
|
||||||
|
)
|
||||||
|
from hexathon import add_0x
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.eth.transaction import EIP155Transaction
|
||||||
|
from funga.error import (
|
||||||
|
UnknownAccountError,
|
||||||
|
SignerError,
|
||||||
|
)
|
||||||
|
from funga.eth.cli.jsonrpc import jsonrpc_ok
|
||||||
|
from .jsonrpc import (
|
||||||
|
jsonrpc_error,
|
||||||
|
is_valid_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SignRequestHandler:
|
||||||
|
|
||||||
|
keystore = None
|
||||||
|
signer = None
|
||||||
|
|
||||||
|
def process_input(self, j):
|
||||||
|
rpc_id = j['id']
|
||||||
|
m = j['method']
|
||||||
|
p = j['params']
|
||||||
|
return (rpc_id, getattr(self, m)(p))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_jsonrpc(self, d):
|
||||||
|
j = None
|
||||||
|
try:
|
||||||
|
j = json.loads(d)
|
||||||
|
is_valid_json(j)
|
||||||
|
logg.debug('{}'.format(d.decode('utf-8')))
|
||||||
|
except Exception as e:
|
||||||
|
logg.exception('input error {}'.format(e))
|
||||||
|
j = json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')
|
||||||
|
raise SignerError(j)
|
||||||
|
|
||||||
|
try:
|
||||||
|
(rpc_id, r) = self.process_input(j)
|
||||||
|
r = jsonrpc_ok(rpc_id, r)
|
||||||
|
j = json.dumps(r).encode('utf-8')
|
||||||
|
except ValueError as e:
|
||||||
|
# TODO: handle cases to give better error context to caller
|
||||||
|
logg.exception('process error {}'.format(e))
|
||||||
|
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
|
||||||
|
raise SignerError(j)
|
||||||
|
except UnknownAccountError as e:
|
||||||
|
logg.exception('process unknown account error {}'.format(e))
|
||||||
|
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
|
||||||
|
raise SignerError(j)
|
||||||
|
|
||||||
|
return j
|
||||||
|
|
||||||
|
|
||||||
|
def personal_newAccount(self, p):
|
||||||
|
password = p
|
||||||
|
if p.__class__.__name__ != 'str':
|
||||||
|
if p.__class__.__name__ != 'list':
|
||||||
|
e = JSONRPCInvalidParams()
|
||||||
|
e.data = 'parameter must be list containing one string'
|
||||||
|
raise ValueError(e)
|
||||||
|
logg.error('foo {}'.format(p))
|
||||||
|
if len(p) != 1:
|
||||||
|
e = JSONRPCInvalidParams()
|
||||||
|
e.data = 'parameter must be list containing one string'
|
||||||
|
raise ValueError(e)
|
||||||
|
if p[0].__class__.__name__ != 'str':
|
||||||
|
e = JSONRPCInvalidParams()
|
||||||
|
e.data = 'parameter must be list containing one string'
|
||||||
|
raise ValueError(e)
|
||||||
|
password = p[0]
|
||||||
|
|
||||||
|
r = self.keystore.new(password)
|
||||||
|
|
||||||
|
return add_0x(r)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: move to translation module ("personal" rpc namespace is node-specific)
|
||||||
|
def personal_signTransaction(self, p):
|
||||||
|
logg.debug('got {} to sign'.format(p[0]))
|
||||||
|
t = EIP155Transaction(p[0], p[0]['nonce'], p[0]['chainId'])
|
||||||
|
raw_signed_tx = self.signer.sign_transaction_to_rlp(t, p[1])
|
||||||
|
o = {
|
||||||
|
'raw': '0x' + raw_signed_tx.hex(),
|
||||||
|
'tx': t.serialize(),
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
def eth_signTransaction(self, tx):
|
||||||
|
o = self.personal_signTransaction([tx[0], ''])
|
||||||
|
return o['raw']
|
||||||
|
|
||||||
|
|
||||||
|
def eth_sign(self, p):
|
||||||
|
logg.debug('got message {} to sign'.format(p[1]))
|
||||||
|
message_type = type(p[1]).__name__
|
||||||
|
if message_type != 'str':
|
||||||
|
raise ValueError('invalid message format, must be {}, not {}'.format(message_type))
|
||||||
|
z = self.signer.sign_ethereum_message(p[0], p[1][2:])
|
||||||
|
return add_0x(z.hex())
|
||||||
|
|
85
funga/eth/cli/http.py
Normal file
85
funga/eth/cli/http.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from http.server import (
|
||||||
|
HTTPServer,
|
||||||
|
BaseHTTPRequestHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from .handle import SignRequestHandler
|
||||||
|
from crypto_dev_signer.error import SignerError
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server_http(spec):
|
||||||
|
httpd = HTTPServer(spec, HTTPSignRequestHandler)
|
||||||
|
logg.debug('starting http server {}'.format(spec))
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSignRequestHandler(SignRequestHandler, BaseHTTPRequestHandler):
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.headers.get('Content-Type') != 'application/json':
|
||||||
|
self.send_response(400, 'me read json only')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'application/json' not in self.headers.get('Accept').split(','):
|
||||||
|
self.send_response(400, 'me json only speak')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
l = self.headers.get('Content-Length')
|
||||||
|
try:
|
||||||
|
l = int(l)
|
||||||
|
except ValueError:
|
||||||
|
self.send_response(400, 'content length must be integer')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
if l > 4096:
|
||||||
|
self.send_response(400, 'too much information')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
if l < 0:
|
||||||
|
self.send_response(400, 'you are too negative')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
b = b''
|
||||||
|
c = 0
|
||||||
|
while c < l:
|
||||||
|
d = self.rfile.read(l-c)
|
||||||
|
if d == None:
|
||||||
|
break
|
||||||
|
b += d
|
||||||
|
c += len(d)
|
||||||
|
if c > 4096:
|
||||||
|
self.send_response(413, 'i should slap you around for lying about your size')
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = self.handle_jsonrpc(d)
|
||||||
|
except SignerError as e:
|
||||||
|
r = e.to_jsonrpc()
|
||||||
|
|
||||||
|
l = len(r)
|
||||||
|
self.send_response(200, 'You are the Keymaster')
|
||||||
|
self.send_header('Content-Length', str(l))
|
||||||
|
self.send_header('Cache-Control', 'no-cache')
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
c = 0
|
||||||
|
while c < l:
|
||||||
|
n = self.wfile.write(r[c:])
|
||||||
|
c += n
|
||||||
|
|
||||||
|
|
30
funga/eth/cli/jsonrpc.py
Normal file
30
funga/eth/cli/jsonrpc.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# local imports
|
||||||
|
from funga.error import UnknownAccountError
|
||||||
|
|
||||||
|
|
||||||
|
def jsonrpc_error(rpc_id, err):
|
||||||
|
return {
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'id': rpc_id,
|
||||||
|
'error': {
|
||||||
|
'code': err.CODE,
|
||||||
|
'message': err.MESSAGE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def jsonrpc_ok(rpc_id, response):
|
||||||
|
return {
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'id': rpc_id,
|
||||||
|
'result': response,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_json(j):
|
||||||
|
if j.get('id') == 'None':
|
||||||
|
raise ValueError('id missing')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
67
funga/eth/cli/socket.py
Normal file
67
funga/eth/cli/socket.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import stat
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from crypto_dev_signer.error import SignerError
|
||||||
|
from .handle import SignRequestHandler
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SocketHandler:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handler = SignRequestHandler()
|
||||||
|
|
||||||
|
|
||||||
|
def process(self, csock):
|
||||||
|
d = csock.recv(4096)
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
r = self.handler.handle_jsonrpc(d)
|
||||||
|
except SignerError as e:
|
||||||
|
r = e.to_jsonrpc()
|
||||||
|
|
||||||
|
csock.send(r)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server_socket(s):
|
||||||
|
s.listen(10)
|
||||||
|
logg.debug('server started')
|
||||||
|
handler = SocketHandler()
|
||||||
|
while True:
|
||||||
|
(csock, caddr) = s.accept()
|
||||||
|
handler.process(csock)
|
||||||
|
csock.close()
|
||||||
|
s.close()
|
||||||
|
os.unlink(socket_path)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server_tcp(spec):
|
||||||
|
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||||
|
s.bind(spec)
|
||||||
|
logg.debug('created tcp socket {}'.format(spec))
|
||||||
|
start_server_socket(s)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server_unix(socket_path):
|
||||||
|
socket_dir = os.path.dirname(socket_path)
|
||||||
|
try:
|
||||||
|
fi = os.stat(socket_dir)
|
||||||
|
if not stat.S_ISDIR:
|
||||||
|
RuntimeError('socket path {} is not a directory'.format(socket_dir))
|
||||||
|
except FileNotFoundError:
|
||||||
|
os.mkdir(socket_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink(socket_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
s = socket.socket(family = socket.AF_UNIX, type = socket.SOCK_STREAM)
|
||||||
|
s.bind(socket_path)
|
||||||
|
logg.debug('created unix ipc socket {}'.format(socket_path))
|
||||||
|
start_server_socket(s)
|
91
funga/eth/encoding.py
Normal file
91
funga/eth/encoding.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
import coincurve
|
||||||
|
import sha3
|
||||||
|
from hexathon import (
|
||||||
|
strip_0x,
|
||||||
|
uniform,
|
||||||
|
)
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def private_key_from_bytes(b):
|
||||||
|
return coincurve.PrivateKey(secret=b)
|
||||||
|
|
||||||
|
|
||||||
|
def public_key_bytes_to_address(pubk_bytes, result_format='hex'):
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
logg.debug('public key bytes {}'.format(pubk_bytes.hex()))
|
||||||
|
h.update(pubk_bytes[1:])
|
||||||
|
z = h.digest()[12:]
|
||||||
|
if result_format == 'hex':
|
||||||
|
return to_checksum_address(z[:20].hex())
|
||||||
|
elif result_format == 'bytes':
|
||||||
|
return z[:20]
|
||||||
|
raise ValueError('invalid result format "{}"'.format(result_format))
|
||||||
|
|
||||||
|
|
||||||
|
def public_key_to_address(pubk, result_format='hex'):
|
||||||
|
pubk_bytes = pubk.format(compressed=False)
|
||||||
|
return public_key_bytes_to_address(pubk_bytes, result_format='hex')
|
||||||
|
|
||||||
|
|
||||||
|
def private_key_to_address(pk, result_format='hex'):
|
||||||
|
pubk = coincurve.PublicKey.from_secret(pk.secret)
|
||||||
|
#logg.debug('secret {} '.format(pk.secret.hex()))
|
||||||
|
return public_key_to_address(pubk, result_format)
|
||||||
|
|
||||||
|
|
||||||
|
def is_address(address_hex):
|
||||||
|
try:
|
||||||
|
address_hex = strip_0x(address_hex)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return len(address_hex) == 40
|
||||||
|
|
||||||
|
|
||||||
|
def is_checksum_address(address_hex):
|
||||||
|
hx = None
|
||||||
|
try:
|
||||||
|
hx = to_checksum(address_hex)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return hx == strip_0x(address_hex)
|
||||||
|
|
||||||
|
|
||||||
|
def to_checksum_address(address_hex):
|
||||||
|
address_hex = strip_0x(address_hex)
|
||||||
|
address_hex = uniform(address_hex)
|
||||||
|
if len(address_hex) != 40:
|
||||||
|
raise ValueError('Invalid address length')
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
h.update(address_hex.encode('utf-8'))
|
||||||
|
z = h.digest()
|
||||||
|
|
||||||
|
#checksum_address_hex = '0x'
|
||||||
|
checksum_address_hex = ''
|
||||||
|
|
||||||
|
for (i, c) in enumerate(address_hex):
|
||||||
|
if c in '1234567890':
|
||||||
|
checksum_address_hex += c
|
||||||
|
elif c in 'abcdef':
|
||||||
|
if z[int(i / 2)] & (0x80 >> ((i % 2) * 4)) > 1:
|
||||||
|
checksum_address_hex += c.upper()
|
||||||
|
else:
|
||||||
|
checksum_address_hex += c
|
||||||
|
|
||||||
|
return checksum_address_hex
|
||||||
|
|
||||||
|
to_checksum = to_checksum_address
|
||||||
|
|
||||||
|
ethereum_recid_modifier = 35
|
||||||
|
|
||||||
|
def chain_id_to_v(chain_id, signature):
|
||||||
|
v = signature[64]
|
||||||
|
return (chain_id * 2) + ethereum_recid_modifier + v
|
||||||
|
|
||||||
|
def chainv_to_v(chain_id, v):
|
||||||
|
return v - ethereum_recid_modifier - (chain_id * 2)
|
1
funga/eth/helper/__init__.py
Normal file
1
funga/eth/helper/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .tx import EthTxExecutor
|
58
funga/eth/helper/tx.py
Normal file
58
funga/eth/helper/tx.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from crypto_dev_signer.helper import TxExecutor
|
||||||
|
from crypto_dev_signer.error import NetworkError
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
logging.getLogger('web3').setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger('urllib3').setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
class EthTxExecutor(TxExecutor):
|
||||||
|
|
||||||
|
def __init__(self, w3, sender, signer, chain_id, verifier=None, block=False):
|
||||||
|
self.w3 = w3
|
||||||
|
nonce = self.w3.eth.getTransactionCount(sender, 'pending')
|
||||||
|
super(EthTxExecutor, self).__init__(sender, signer, self.translator, self.dispatcher, self.reporter, nonce, chain_id, self.fee_helper, self.fee_price_helper, verifier, block)
|
||||||
|
|
||||||
|
|
||||||
|
def fee_helper(self, tx):
|
||||||
|
estimate = self.w3.eth.estimateGas(tx)
|
||||||
|
if estimate < 21000:
|
||||||
|
estimate = 21000
|
||||||
|
logg.debug('estimate {} {}'.format(tx, estimate))
|
||||||
|
return estimate
|
||||||
|
|
||||||
|
|
||||||
|
def fee_price_helper(self):
|
||||||
|
return self.w3.eth.gasPrice
|
||||||
|
|
||||||
|
|
||||||
|
def dispatcher(self, tx):
|
||||||
|
error_object = None
|
||||||
|
try:
|
||||||
|
tx_hash = self.w3.eth.sendRawTransaction(tx)
|
||||||
|
except ValueError as e:
|
||||||
|
error_object = e.args[0]
|
||||||
|
logg.error('node could not intepret rlp {}'.format(tx))
|
||||||
|
if error_object != None:
|
||||||
|
raise NetworkError(error_object)
|
||||||
|
return tx_hash
|
||||||
|
|
||||||
|
|
||||||
|
def reporter(self, tx):
|
||||||
|
return self.w3.eth.getTransactionReceipt(tx)
|
||||||
|
|
||||||
|
|
||||||
|
def translator(self, tx):
|
||||||
|
if tx.get('feePrice') != None:
|
||||||
|
tx['gasPrice'] = tx['feePrice']
|
||||||
|
del tx['feePrice']
|
||||||
|
|
||||||
|
if tx.get('feeUnits') != None:
|
||||||
|
tx['gas'] = tx['feeUnits']
|
||||||
|
del tx['feeUnits']
|
||||||
|
|
||||||
|
return tx
|
8
funga/eth/keystore/__init__.py
Normal file
8
funga/eth/keystore/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# third-party imports
|
||||||
|
#from eth_keys import KeyAPI
|
||||||
|
#from eth_keys.backends import NativeECCBackend
|
||||||
|
|
||||||
|
#keyapi = KeyAPI(NativeECCBackend)
|
||||||
|
|
||||||
|
#from .postgres import ReferenceKeystore
|
||||||
|
#from .dict import DictKeystore
|
45
funga/eth/keystore/dict.py
Normal file
45
funga/eth/keystore/dict.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from hexathon import (
|
||||||
|
strip_0x,
|
||||||
|
add_0x,
|
||||||
|
)
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
#from . import keyapi
|
||||||
|
from funga.error import UnknownAccountError
|
||||||
|
from .interface import EthKeystore
|
||||||
|
from funga.eth.encoding import private_key_to_address
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DictKeystore(EthKeystore):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(DictKeystore, self).__init__()
|
||||||
|
self.keys = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, address, password=None):
|
||||||
|
address_key = strip_0x(address).lower()
|
||||||
|
if password != None:
|
||||||
|
logg.debug('password ignored as dictkeystore doesnt do encryption')
|
||||||
|
try:
|
||||||
|
return self.keys[address_key]
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownAccountError(address_key)
|
||||||
|
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
return list(self.keys.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def import_key(self, pk, password=None):
|
||||||
|
address_hex = private_key_to_address(pk)
|
||||||
|
address_hex_clean = strip_0x(address_hex).lower()
|
||||||
|
self.keys[address_hex_clean] = pk.secret
|
||||||
|
logg.debug('added key {}'.format(address_hex))
|
||||||
|
return add_0x(address_hex)
|
50
funga/eth/keystore/interface.py
Normal file
50
funga/eth/keystore/interface.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.eth.keystore import keyfile
|
||||||
|
from funga.eth.encoding import private_key_from_bytes
|
||||||
|
from funga.keystore import Keystore
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def native_keygen(*args, **kwargs):
|
||||||
|
return os.urandom(32)
|
||||||
|
|
||||||
|
|
||||||
|
class EthKeystore(Keystore):
|
||||||
|
|
||||||
|
def __init__(self, private_key_generator=native_keygen):
|
||||||
|
super(EthKeystore, self).__init__(private_key_generator, private_key_from_bytes, keyfile.from_some)
|
||||||
|
|
||||||
|
|
||||||
|
def new(self, password=None):
|
||||||
|
b = self.private_key_generator()
|
||||||
|
return self.import_raw_key(b, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def import_raw_key(self, b, password=None):
|
||||||
|
pk = private_key_from_bytes(b)
|
||||||
|
return self.import_key(pk, password)
|
||||||
|
|
||||||
|
|
||||||
|
def import_key(self, pk, password=None):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def import_keystore_data(self, keystore_content, password=''):
|
||||||
|
if type(keystore_content).__name__ == 'str':
|
||||||
|
keystore_content = json.loads(keystore_content)
|
||||||
|
elif type(keystore_content).__name__ == 'bytes':
|
||||||
|
logg.debug('bytes {}'.format(keystore_content))
|
||||||
|
keystore_content = json.loads(keystore_content.decode('utf-8'))
|
||||||
|
private_key = keyfile.from_dict(keystore_content, password.encode('utf-8'))
|
||||||
|
return self.import_raw_key(private_key, password)
|
||||||
|
|
||||||
|
|
||||||
|
def import_keystore_file(self, keystore_file, password=''):
|
||||||
|
private_key = keyfile.from_file(keystore_file, password)
|
||||||
|
return self.import_raw_key(private_key)
|
173
funga/eth/keystore/keyfile.py
Normal file
173
funga/eth/keystore/keyfile.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
import coincurve
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util import Counter
|
||||||
|
import sha3
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.error import (
|
||||||
|
DecryptError,
|
||||||
|
KeyfileError,
|
||||||
|
)
|
||||||
|
from funga.eth.encoding import private_key_to_address
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
algo_keywords = [
|
||||||
|
'aes-128-ctr',
|
||||||
|
]
|
||||||
|
hash_keywords = [
|
||||||
|
'scrypt'
|
||||||
|
]
|
||||||
|
|
||||||
|
default_kdfparams = {
|
||||||
|
'dklen': 32,
|
||||||
|
'n': 1 << 18,
|
||||||
|
'p': 1,
|
||||||
|
'r': 8,
|
||||||
|
'salt': os.urandom(32).hex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_mac(mac_key, ciphertext_bytes):
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
h.update(mac_key)
|
||||||
|
h.update(ciphertext_bytes)
|
||||||
|
return h.digest()
|
||||||
|
|
||||||
|
|
||||||
|
class Hashes:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_scrypt(kdfparams=default_kdfparams, passphrase=''):
|
||||||
|
dklen = int(kdfparams['dklen'])
|
||||||
|
n = int(kdfparams['n'])
|
||||||
|
p = int(kdfparams['p'])
|
||||||
|
r = int(kdfparams['r'])
|
||||||
|
salt = bytes.fromhex(kdfparams['salt'])
|
||||||
|
|
||||||
|
return hashlib.scrypt(passphrase.encode('utf-8'), salt=salt,n=n, p=p, r=r, maxmem=1024*1024*1024, dklen=dklen)
|
||||||
|
|
||||||
|
|
||||||
|
class Ciphers:
|
||||||
|
|
||||||
|
aes_128_block_size = 1 << 7
|
||||||
|
aes_iv_len = 16
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_aes_128_ctr(ciphertext, decryption_key, iv):
|
||||||
|
ctr = Counter.new(Ciphers.aes_128_block_size, initial_value=iv)
|
||||||
|
cipher = AES.new(decryption_key, AES.MODE_CTR, counter=ctr)
|
||||||
|
plaintext = cipher.decrypt(ciphertext)
|
||||||
|
return plaintext
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encrypt_aes_128_ctr(plaintext, encryption_key, iv):
|
||||||
|
ctr = Counter.new(Ciphers.aes_128_block_size, initial_value=iv)
|
||||||
|
cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr)
|
||||||
|
ciphertext = cipher.encrypt(plaintext)
|
||||||
|
return ciphertext
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(private_key_bytes, passphrase=''):
|
||||||
|
|
||||||
|
private_key = coincurve.PrivateKey(secret=private_key_bytes)
|
||||||
|
|
||||||
|
encryption_key = Hashes.from_scrypt(passphrase=passphrase)
|
||||||
|
|
||||||
|
address_hex = private_key_to_address(private_key)
|
||||||
|
iv_bytes = os.urandom(Ciphers.aes_iv_len)
|
||||||
|
iv = int.from_bytes(iv_bytes, 'big')
|
||||||
|
ciphertext_bytes = Ciphers.encrypt_aes_128_ctr(private_key.secret, encryption_key[:16], iv)
|
||||||
|
|
||||||
|
mac = to_mac(encryption_key[16:], ciphertext_bytes)
|
||||||
|
|
||||||
|
crypto_dict = {
|
||||||
|
'cipher': 'aes-128-ctr',
|
||||||
|
'ciphertext': ciphertext_bytes.hex(),
|
||||||
|
'cipherparams': {
|
||||||
|
'iv': iv_bytes.hex(),
|
||||||
|
},
|
||||||
|
'kdf': 'scrypt',
|
||||||
|
'kdfparams': default_kdfparams,
|
||||||
|
'mac': mac.hex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
uu = uuid.uuid1()
|
||||||
|
o = {
|
||||||
|
'address': address_hex,
|
||||||
|
'version': 3,
|
||||||
|
'crypto': crypto_dict,
|
||||||
|
'id': str(uu),
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
def from_dict(o, passphrase=''):
|
||||||
|
|
||||||
|
cipher = o['crypto']['cipher']
|
||||||
|
if cipher not in algo_keywords:
|
||||||
|
raise NotImplementedError('cipher "{}" not implemented'.format(cipher))
|
||||||
|
|
||||||
|
kdf = o['crypto']['kdf']
|
||||||
|
if kdf not in hash_keywords:
|
||||||
|
raise NotImplementedError('kdf "{}" not implemented'.format(kdf))
|
||||||
|
|
||||||
|
m = getattr(Hashes, 'from_{}'.format(kdf.replace('-', '_')))
|
||||||
|
decryption_key = m(o['crypto']['kdfparams'], passphrase)
|
||||||
|
|
||||||
|
control_mac = bytes.fromhex(o['crypto']['mac'])
|
||||||
|
iv_bytes = bytes.fromhex(o['crypto']['cipherparams']['iv'])
|
||||||
|
iv = int.from_bytes(iv_bytes, "big")
|
||||||
|
ciphertext_bytes = bytes.fromhex(o['crypto']['ciphertext'])
|
||||||
|
|
||||||
|
# check mac
|
||||||
|
calculated_mac = to_mac(decryption_key[16:], ciphertext_bytes)
|
||||||
|
if control_mac != calculated_mac:
|
||||||
|
raise DecryptError('mac mismatch when decrypting passphrase')
|
||||||
|
|
||||||
|
m = getattr(Ciphers, 'decrypt_{}'.format(cipher.replace('-', '_')))
|
||||||
|
|
||||||
|
try:
|
||||||
|
pk = m(ciphertext_bytes, decryption_key[:16], iv)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise DecryptError('could not decrypt keyfile: {}'.format(e))
|
||||||
|
logg.debug('bar')
|
||||||
|
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
def from_file(filepath, passphrase=''):
|
||||||
|
|
||||||
|
f = open(filepath, 'r')
|
||||||
|
try:
|
||||||
|
o = json.load(f)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
f.close()
|
||||||
|
raise KeyfileError(e)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return from_dict(o, passphrase)
|
||||||
|
|
||||||
|
|
||||||
|
def from_some(v, passphrase=''):
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
v = v.decode('utf-8')
|
||||||
|
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return from_file(v, passphrase)
|
||||||
|
except Exception:
|
||||||
|
logg.debug('keyfile parse as file fail')
|
||||||
|
pass
|
||||||
|
v = json.loads(v)
|
||||||
|
|
||||||
|
return from_dict(v, passphrase)
|
108
funga/eth/keystore/sql.py
Normal file
108
funga/eth/keystore/sql.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
#import psycopg2
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
import sha3
|
||||||
|
from hexathon import (
|
||||||
|
strip_0x,
|
||||||
|
add_0x,
|
||||||
|
)
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from .interface import EthKeystore
|
||||||
|
#from . import keyapi
|
||||||
|
from funga.error import UnknownAccountError
|
||||||
|
from funga.eth.encoding import private_key_to_address
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def to_bytes(x):
|
||||||
|
return x.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class SQLKeystore(EthKeystore):
|
||||||
|
|
||||||
|
schema = [
|
||||||
|
"""CREATE TABLE IF NOT EXISTS ethereum (
|
||||||
|
id SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
key_ciphertext VARCHAR(256) NOT NULL,
|
||||||
|
wallet_address_hex CHAR(40) NOT NULL
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
"""CREATE UNIQUE INDEX IF NOT EXISTS ethereum_address_idx ON ethereum ( wallet_address_hex );
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, dsn, **kwargs):
|
||||||
|
super(SQLKeystore, self).__init__()
|
||||||
|
logg.debug('starting db session with dsn {}'.format(dsn))
|
||||||
|
self.db_engine = create_engine(dsn)
|
||||||
|
self.db_session = sessionmaker(bind=self.db_engine)()
|
||||||
|
for s in self.schema:
|
||||||
|
self.db_session.execute(s)
|
||||||
|
self.db_session.commit()
|
||||||
|
self.symmetric_key = kwargs.get('symmetric_key')
|
||||||
|
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
logg.debug('closing db session')
|
||||||
|
self.db_session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, address, password=None):
|
||||||
|
safe_address = strip_0x(address).lower()
|
||||||
|
s = text('SELECT key_ciphertext FROM ethereum WHERE wallet_address_hex = :a')
|
||||||
|
r = self.db_session.execute(s, {
|
||||||
|
'a': safe_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
k = r.first()[0]
|
||||||
|
except TypeError:
|
||||||
|
self.db_session.rollback()
|
||||||
|
raise UnknownAccountError(safe_address)
|
||||||
|
self.db_session.commit()
|
||||||
|
a = self._decrypt(k, password)
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def import_key(self, pk, password=None):
|
||||||
|
address_hex = private_key_to_address(pk)
|
||||||
|
address_hex_clean = strip_0x(address_hex).lower()
|
||||||
|
|
||||||
|
c = self._encrypt(pk.secret, password)
|
||||||
|
s = text('INSERT INTO ethereum (wallet_address_hex, key_ciphertext) VALUES (:a, :c)') #%s, %s)')
|
||||||
|
self.db_session.execute(s, {
|
||||||
|
'a': address_hex_clean,
|
||||||
|
'c': c.decode('utf-8'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.db_session.commit()
|
||||||
|
logg.info('added private key for address {}'.format(address_hex_clean))
|
||||||
|
return add_0x(address_hex)
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt(self, private_key, password):
|
||||||
|
f = self._generate_encryption_engine(password)
|
||||||
|
return f.encrypt(private_key)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_encryption_engine(self, password):
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
h.update(self.symmetric_key)
|
||||||
|
if password != None:
|
||||||
|
password_bytes = to_bytes(password)
|
||||||
|
h.update(password_bytes)
|
||||||
|
g = h.digest()
|
||||||
|
return Fernet(base64.b64encode(g))
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt(self, c, password):
|
||||||
|
f = self._generate_encryption_engine(password)
|
||||||
|
return f.decrypt(c.encode('utf-8'))
|
87
funga/eth/runnable/keyfile.py
Normal file
87
funga/eth/runnable/keyfile.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
# external impors
|
||||||
|
import coincurve
|
||||||
|
from hexathon import strip_0x
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.error import DecryptError
|
||||||
|
from funga.eth.keystore.keyfile import (
|
||||||
|
from_file,
|
||||||
|
to_dict,
|
||||||
|
)
|
||||||
|
from funga.eth.encoding import (
|
||||||
|
private_key_to_address,
|
||||||
|
private_key_from_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
argparser = argparse.ArgumentParser()
|
||||||
|
argparser.add_argument('-d', '--decrypt', dest='d', type=str, help='decrypt file')
|
||||||
|
argparser.add_argument('--private-key', dest='private_key', action='store_true', help='output private key instead of address')
|
||||||
|
argparser.add_argument('-z', action='store_true', help='zero-length password')
|
||||||
|
argparser.add_argument('-k', type=str, help='load key from file')
|
||||||
|
argparser.add_argument('-v', action='store_true', help='be verbose')
|
||||||
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
if args.v:
|
||||||
|
logg.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
mode = 'create'
|
||||||
|
secret = False
|
||||||
|
if args.d:
|
||||||
|
mode = 'decrypt'
|
||||||
|
if args.private_key:
|
||||||
|
secret = True
|
||||||
|
|
||||||
|
pk_hex = os.environ.get('PRIVATE_KEY')
|
||||||
|
if args.k != None:
|
||||||
|
f = open(args.k, 'r')
|
||||||
|
pk_hex = f.read(66)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global pk_hex
|
||||||
|
|
||||||
|
passphrase = os.environ.get('PASSPHRASE')
|
||||||
|
if args.z:
|
||||||
|
passphrase = ''
|
||||||
|
r = None
|
||||||
|
if mode == 'decrypt':
|
||||||
|
if passphrase == None:
|
||||||
|
passphrase = getpass.getpass('decryption phrase: ')
|
||||||
|
try:
|
||||||
|
r = from_file(args.d, passphrase).hex()
|
||||||
|
except DecryptError:
|
||||||
|
sys.stderr.write('Invalid passphrase\n')
|
||||||
|
sys.exit(1)
|
||||||
|
if not secret:
|
||||||
|
pk = private_key_from_bytes(bytes.fromhex(r))
|
||||||
|
r = private_key_to_address(pk)
|
||||||
|
elif mode == 'create':
|
||||||
|
if passphrase == None:
|
||||||
|
passphrase = getpass.getpass('encryption phrase: ')
|
||||||
|
pk_bytes = None
|
||||||
|
if pk_hex != None:
|
||||||
|
pk_hex = strip_0x(pk_hex)
|
||||||
|
pk_bytes = bytes.fromhex(pk_hex)
|
||||||
|
else:
|
||||||
|
pk_bytes = os.urandom(32)
|
||||||
|
pk = coincurve.PrivateKey(secret=pk_bytes)
|
||||||
|
o = to_dict(pk_bytes, passphrase)
|
||||||
|
r = json.dumps(o)
|
||||||
|
|
||||||
|
print(r)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
122
funga/eth/runnable/signer.py
Executable file
122
funga/eth/runnable/signer.py
Executable file
@ -0,0 +1,122 @@
|
|||||||
|
# standard imports
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
import confini
|
||||||
|
from jsonrpc.exceptions import *
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from crypto_dev_signer.eth.signer import ReferenceSigner
|
||||||
|
from crypto_dev_signer.keystore.reference import ReferenceKeystore
|
||||||
|
from crypto_dev_signer.cli.handle import SignRequestHandler
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
config_dir = '.'
|
||||||
|
|
||||||
|
db = None
|
||||||
|
signer = None
|
||||||
|
session = None
|
||||||
|
chainId = 8995
|
||||||
|
socket_path = '/run/crypto-dev-signer/jsonrpc.ipc'
|
||||||
|
|
||||||
|
argparser = argparse.ArgumentParser()
|
||||||
|
argparser.add_argument('-c', type=str, default=config_dir, help='config file')
|
||||||
|
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
|
||||||
|
argparser.add_argument('-i', type=int, help='default chain id for EIP155')
|
||||||
|
argparser.add_argument('-s', type=str, help='socket path')
|
||||||
|
argparser.add_argument('-v', action='store_true', help='be verbose')
|
||||||
|
argparser.add_argument('-vv', action='store_true', help='be more verbose')
|
||||||
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
if args.vv:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
elif args.v:
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
|
config = confini.Config(args.c, args.env_prefix)
|
||||||
|
config.process()
|
||||||
|
config.censor('PASSWORD', 'DATABASE')
|
||||||
|
config.censor('SECRET', 'SIGNER')
|
||||||
|
logg.debug('config loaded from {}:\n{}'.format(config_dir, config))
|
||||||
|
|
||||||
|
if args.i:
|
||||||
|
chainId = args.i
|
||||||
|
if args.s:
|
||||||
|
socket_url = urlparse(args.s)
|
||||||
|
elif config.get('SIGNER_SOCKET_PATH'):
|
||||||
|
socket_url = urlparse(config.get('SIGNER_SOCKET_PATH'))
|
||||||
|
|
||||||
|
|
||||||
|
# connect to database
|
||||||
|
dsn = 'postgresql://{}:{}@{}:{}/{}'.format(
|
||||||
|
config.get('DATABASE_USER'),
|
||||||
|
config.get('DATABASE_PASSWORD'),
|
||||||
|
config.get('DATABASE_HOST'),
|
||||||
|
config.get('DATABASE_PORT'),
|
||||||
|
config.get('DATABASE_NAME'),
|
||||||
|
)
|
||||||
|
|
||||||
|
logg.info('using dsn {}'.format(dsn))
|
||||||
|
logg.info('using socket {}'.format(config.get('SIGNER_SOCKET_PATH')))
|
||||||
|
|
||||||
|
re_http = r'^http'
|
||||||
|
re_tcp = r'^tcp'
|
||||||
|
re_unix = r'^ipc'
|
||||||
|
|
||||||
|
class MissingSecretError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
secret_hex = config.get('SIGNER_SECRET')
|
||||||
|
if secret_hex == None:
|
||||||
|
raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable')
|
||||||
|
|
||||||
|
secret = bytes.fromhex(secret_hex)
|
||||||
|
kw = {
|
||||||
|
'symmetric_key': secret,
|
||||||
|
}
|
||||||
|
SignRequestHandler.keystore = ReferenceKeystore(dsn, **kw)
|
||||||
|
SignRequestHandler.signer = ReferenceSigner(SignRequestHandler.keystore)
|
||||||
|
|
||||||
|
arg = None
|
||||||
|
try:
|
||||||
|
arg = json.loads(sys.argv[1])
|
||||||
|
except:
|
||||||
|
logg.info('no json rpc command detected, starting socket server {}'.format(socket_url))
|
||||||
|
scheme = 'ipc'
|
||||||
|
if socket_url.scheme != '':
|
||||||
|
scheme = socket_url.scheme
|
||||||
|
if re.match(re_tcp, socket_url.scheme):
|
||||||
|
from crypto_dev_signer.cli.socket import start_server_tcp
|
||||||
|
socket_spec = socket_url.netloc.split(':')
|
||||||
|
host = socket_spec[0]
|
||||||
|
port = int(socket_spec[1])
|
||||||
|
start_server_tcp((host, port))
|
||||||
|
elif re.match(re_http, socket_url.scheme):
|
||||||
|
from crypto_dev_signer.cli.http import start_server_http
|
||||||
|
socket_spec = socket_url.netloc.split(':')
|
||||||
|
host = socket_spec[0]
|
||||||
|
port = int(socket_spec[1])
|
||||||
|
start_server_http((host, port))
|
||||||
|
else:
|
||||||
|
from crypto_dev_signer.cli.socket import start_server_unix
|
||||||
|
start_server_unix(socket_url.path)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
(rpc_id, response) = process_input(arg)
|
||||||
|
r = jsonrpc_ok(rpc_id, response)
|
||||||
|
sys.stdout.write(json.dumps(r))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
1
funga/eth/signer/__init__.py
Normal file
1
funga/eth/signer/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from funga.eth.signer.defaultsigner import EIP155Signer
|
79
funga/eth/signer/defaultsigner.py
Normal file
79
funga/eth/signer/defaultsigner.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
import sha3
|
||||||
|
import coincurve
|
||||||
|
from hexathon import int_to_minbytes
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.signer import Signer
|
||||||
|
from funga.eth.encoding import chain_id_to_v
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EIP155Signer(Signer):
|
||||||
|
|
||||||
|
def __init__(self, keyGetter):
|
||||||
|
super(EIP155Signer, self).__init__(keyGetter)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_transaction(self, tx, password=None):
|
||||||
|
s = tx.rlp_serialize()
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
h.update(s)
|
||||||
|
message_to_sign = h.digest()
|
||||||
|
z = self.sign_pure(tx.sender, message_to_sign, password)
|
||||||
|
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
def sign_transaction_to_rlp(self, tx, password=None):
|
||||||
|
chain_id = int.from_bytes(tx.v, byteorder='big')
|
||||||
|
sig = self.sign_transaction(tx, password)
|
||||||
|
tx.apply_signature(chain_id, sig)
|
||||||
|
return tx.rlp_serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def sign_transaction_to_wire(self, tx, password=None):
|
||||||
|
return self.sign_transaction_to_rlp(tx, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_ethereum_message(self, address, message, password=None):
|
||||||
|
|
||||||
|
#k = keys.PrivateKey(self.keyGetter.get(address, password))
|
||||||
|
#z = keys.ecdsa_sign(message_hash=g, private_key=k)
|
||||||
|
if type(message).__name__ == 'str':
|
||||||
|
logg.debug('signing message in "str" format: {}'.format(message))
|
||||||
|
#z = k.sign_msg(bytes.fromhex(message))
|
||||||
|
message = bytes.fromhex(message)
|
||||||
|
elif type(message).__name__ == 'bytes':
|
||||||
|
logg.debug('signing message in "bytes" format: {}'.format(message.hex()))
|
||||||
|
#z = k.sign_msg(message)
|
||||||
|
else:
|
||||||
|
logg.debug('unhandled format {}'.format(type(message).__name__))
|
||||||
|
raise ValueError('message must be type str or bytes, received {}'.format(type(message).__name__))
|
||||||
|
|
||||||
|
ethereumed_message_header = b'\x19' + 'Ethereum Signed Message:\n{}'.format(len(message)).encode('utf-8')
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
h.update(ethereumed_message_header + message)
|
||||||
|
message_to_sign = h.digest()
|
||||||
|
|
||||||
|
z = self.sign_pure(address, message_to_sign, password)
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: generic sign should be moved to non-eth context
|
||||||
|
def sign_pure(self, address, message, password=None):
|
||||||
|
pk = coincurve.PrivateKey(secret=self.keyGetter.get(address, password))
|
||||||
|
z = pk.sign_recoverable(hasher=None, message=message)
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
def sign_message(self, address, message, password=None, dialect='eth'):
|
||||||
|
if dialect == None:
|
||||||
|
return self.sign_pure(address, message, password=password)
|
||||||
|
elif dialect == 'eth':
|
||||||
|
return self.sign_ethereum_message(address, message, password=password)
|
||||||
|
raise ValueError('Unknown message sign dialect "{}"'.format(dialect))
|
172
funga/eth/transaction.py
Normal file
172
funga/eth/transaction.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
import re
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
#from rlp import encode as rlp_encode
|
||||||
|
from hexathon import (
|
||||||
|
strip_0x,
|
||||||
|
add_0x,
|
||||||
|
int_to_minbytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.eth.encoding import chain_id_to_v
|
||||||
|
#from crypto_dev_signer.eth.rlp import rlp_encode
|
||||||
|
import rlp
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
rlp_encode = rlp.encode
|
||||||
|
|
||||||
|
class Transaction:
|
||||||
|
|
||||||
|
def rlp_serialize(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class EIP155Transaction:
|
||||||
|
|
||||||
|
def __init__(self, tx, nonce_in, chainId_in=1):
|
||||||
|
to = b''
|
||||||
|
data = b''
|
||||||
|
if tx.get('to') != None:
|
||||||
|
to = bytes.fromhex(strip_0x(tx['to'], allow_empty=True))
|
||||||
|
if tx.get('data') != None:
|
||||||
|
data = bytes.fromhex(strip_0x(tx['data'], allow_empty=True))
|
||||||
|
|
||||||
|
gas_price = None
|
||||||
|
start_gas = None
|
||||||
|
value = None
|
||||||
|
nonce = None
|
||||||
|
chainId = None
|
||||||
|
|
||||||
|
# TODO: go directly from hex to bytes
|
||||||
|
try:
|
||||||
|
gas_price = int(tx['gasPrice'])
|
||||||
|
byts = ((gas_price.bit_length()-1)/8)+1
|
||||||
|
gas_price = gas_price.to_bytes(int(byts), 'big')
|
||||||
|
except ValueError:
|
||||||
|
gas_price = bytes.fromhex(strip_0x(tx['gasPrice'], allow_empty=True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_gas = int(tx['gas'])
|
||||||
|
byts = ((start_gas.bit_length()-1)/8)+1
|
||||||
|
start_gas = start_gas.to_bytes(int(byts), 'big')
|
||||||
|
except ValueError:
|
||||||
|
start_gas = bytes.fromhex(strip_0x(tx['gas'], allow_empty=True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = int(tx['value'])
|
||||||
|
byts = ((value.bit_length()-1)/8)+1
|
||||||
|
value = value.to_bytes(int(byts), 'big')
|
||||||
|
except ValueError:
|
||||||
|
value = bytes.fromhex(strip_0x(tx['value'], allow_empty=True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
nonce = int(nonce_in)
|
||||||
|
byts = ((nonce.bit_length()-1)/8)+1
|
||||||
|
nonce = nonce.to_bytes(int(byts), 'big')
|
||||||
|
except ValueError:
|
||||||
|
nonce = bytes.fromhex(strip_0x(nonce_in, allow_empty=True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
chainId = int(chainId_in)
|
||||||
|
byts = ((chainId.bit_length()-1)/8)+1
|
||||||
|
chainId = chainId.to_bytes(int(byts), 'big')
|
||||||
|
except ValueError:
|
||||||
|
chainId = bytes.fromhex(strip_0x(chainId_in, allow_empty=True))
|
||||||
|
|
||||||
|
self.nonce = nonce
|
||||||
|
self.gas_price = gas_price
|
||||||
|
self.start_gas = start_gas
|
||||||
|
self.to = to
|
||||||
|
self.value = value
|
||||||
|
self.data = data
|
||||||
|
self.v = chainId
|
||||||
|
self.r = b''
|
||||||
|
self.s = b''
|
||||||
|
self.sender = strip_0x(tx['from'])
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_order(self):
|
||||||
|
s = [
|
||||||
|
self.nonce,
|
||||||
|
self.gas_price,
|
||||||
|
self.start_gas,
|
||||||
|
self.to,
|
||||||
|
self.value,
|
||||||
|
self.data,
|
||||||
|
self.v,
|
||||||
|
self.r,
|
||||||
|
self.s,
|
||||||
|
]
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_serialize(self):
|
||||||
|
s = self.canonical_order()
|
||||||
|
b = b''
|
||||||
|
for e in s:
|
||||||
|
b += e
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def rlp_serialize(self):
|
||||||
|
s = self.canonical_order()
|
||||||
|
return rlp_encode(s)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
tx = {
|
||||||
|
'nonce': add_0x(self.nonce.hex(), allow_empty=True),
|
||||||
|
'gasPrice': add_0x(self.gas_price.hex()),
|
||||||
|
'gas': add_0x(self.start_gas.hex()),
|
||||||
|
'value': add_0x(self.value.hex(), allow_empty=True),
|
||||||
|
'data': add_0x(self.data.hex(), allow_empty=True),
|
||||||
|
'v': add_0x(self.v.hex(), allow_empty=True),
|
||||||
|
'r': add_0x(self.r.hex(), allow_empty=True),
|
||||||
|
's': add_0x(self.s.hex(), allow_empty=True),
|
||||||
|
}
|
||||||
|
if self.to == None or len(self.to) == 0:
|
||||||
|
tx['to'] = None
|
||||||
|
else:
|
||||||
|
tx['to'] = add_0x(self.to.hex())
|
||||||
|
|
||||||
|
if tx['data'] == '':
|
||||||
|
tx['data'] = '0x'
|
||||||
|
|
||||||
|
if tx['value'] == '':
|
||||||
|
tx['value'] = '0x00'
|
||||||
|
|
||||||
|
if tx['nonce'] == '':
|
||||||
|
tx['nonce'] = '0x00'
|
||||||
|
|
||||||
|
return tx
|
||||||
|
|
||||||
|
|
||||||
|
def apply_signature(self, chain_id, signature, v=None):
|
||||||
|
if len(self.r + self.s) > 0:
|
||||||
|
raise AttributeError('signature already set')
|
||||||
|
if len(signature) < 65:
|
||||||
|
raise ValueError('invalid signature length')
|
||||||
|
if v == None:
|
||||||
|
v = chain_id_to_v(chain_id, signature)
|
||||||
|
self.v = int_to_minbytes(v)
|
||||||
|
self.r = signature[:32]
|
||||||
|
self.s = signature[32:64]
|
||||||
|
|
||||||
|
for i in range(len(self.r)):
|
||||||
|
if self.r[i] > 0:
|
||||||
|
self.r = self.r[i:]
|
||||||
|
break
|
||||||
|
|
||||||
|
for i in range(len(self.s)):
|
||||||
|
if self.s[i] > 0:
|
||||||
|
self.s = self.s[i:]
|
||||||
|
break
|
29
funga/eth/web3ext/__init__.py
Normal file
29
funga/eth/web3ext/__init__.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from web3 import Web3 as Web3super
|
||||||
|
from web3 import WebsocketProvider, HTTPProvider
|
||||||
|
from .middleware import PlatformMiddleware
|
||||||
|
|
||||||
|
re_websocket = re.compile('^wss?://')
|
||||||
|
re_http = re.compile('^https?://')
|
||||||
|
|
||||||
|
logg = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_middleware(ipcpath):
|
||||||
|
PlatformMiddleware.ipcaddr = ipcpath
|
||||||
|
return PlatformMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
# overrides the original Web3 constructor
|
||||||
|
#def Web3(blockchain_provider='ws://localhost:8546', ipcpath=None):
|
||||||
|
def Web3(provider, ipcpath=None):
|
||||||
|
w3 = Web3super(provider)
|
||||||
|
|
||||||
|
if ipcpath != None:
|
||||||
|
logg.info('using signer middleware with ipc {}'.format(ipcpath))
|
||||||
|
w3.middleware_onion.add(create_middleware(ipcpath))
|
||||||
|
|
||||||
|
w3.eth.personal = w3.geth.personal
|
||||||
|
return w3
|
116
funga/eth/web3ext/middleware.py
Normal file
116
funga/eth/web3ext/middleware.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
|
||||||
|
logg = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def jsonrpc_request(method, params):
|
||||||
|
uu = uuid.uuid4()
|
||||||
|
return {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": str(uu),
|
||||||
|
"method": method,
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformMiddleware:
|
||||||
|
|
||||||
|
# id for the request is not available, meaning we cannot easily short-circuit
|
||||||
|
# hack workaround
|
||||||
|
id_seq = -1
|
||||||
|
re_personal = re.compile('^personal_.*')
|
||||||
|
ipcaddr = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, make_request, w3):
|
||||||
|
self.w3 = w3
|
||||||
|
self.make_request = make_request
|
||||||
|
if self.ipcaddr == None:
|
||||||
|
raise AttributeError('ipcaddr not set')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: understand what format input params come in
|
||||||
|
# single entry input gives a tuple on params, wtf...
|
||||||
|
# dict input comes as [{}] and fails if not passed on as an array
|
||||||
|
@staticmethod
|
||||||
|
def _translate_params(params):
|
||||||
|
#if params.__class__.__name__ == 'tuple':
|
||||||
|
# r = []
|
||||||
|
# for p in params:
|
||||||
|
# r.append(p)
|
||||||
|
# return r
|
||||||
|
|
||||||
|
if params.__class__.__name__ == 'list' and len(params) > 0:
|
||||||
|
return params[0]
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: DRY
|
||||||
|
def __call__(self, method, suspect_params):
|
||||||
|
|
||||||
|
self.id_seq += 1
|
||||||
|
logg.debug('in middleware method {} params {} ipcpath {}'.format(method, suspect_params, self.ipcaddr))
|
||||||
|
|
||||||
|
if self.re_personal.match(method) != None:
|
||||||
|
params = PlatformMiddleware._translate_params(suspect_params)
|
||||||
|
# multiple providers is removed in web3.py 5.12.0
|
||||||
|
# https://github.com/ethereum/web3.py/issues/1701
|
||||||
|
# thus we need a workaround to use the same web3 instance
|
||||||
|
s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0)
|
||||||
|
ipc_provider_workaround = s.connect(self.ipcaddr)
|
||||||
|
|
||||||
|
logg.info('redirecting method {} params {} original params {}'.format(method, params, suspect_params))
|
||||||
|
o = jsonrpc_request(method, params[0])
|
||||||
|
j = json.dumps(o)
|
||||||
|
logg.debug('send {}'.format(j))
|
||||||
|
s.send(j.encode('utf-8'))
|
||||||
|
r = s.recv(4096)
|
||||||
|
s.close()
|
||||||
|
logg.debug('got recv {}'.format(str(r)))
|
||||||
|
jr = json.loads(r)
|
||||||
|
jr['id'] = self.id_seq
|
||||||
|
#return str(json.dumps(jr))
|
||||||
|
return jr
|
||||||
|
|
||||||
|
elif method == 'eth_signTransaction':
|
||||||
|
params = PlatformMiddleware._translate_params(suspect_params)
|
||||||
|
s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0)
|
||||||
|
ipc_provider_workaround = s.connect(self.ipcaddr)
|
||||||
|
logg.info('redirecting method {} params {} original params {}'.format(method, params, suspect_params))
|
||||||
|
o = jsonrpc_request(method, params[0])
|
||||||
|
j = json.dumps(o)
|
||||||
|
logg.debug('send {}'.format(j))
|
||||||
|
s.send(j.encode('utf-8'))
|
||||||
|
r = s.recv(4096)
|
||||||
|
s.close()
|
||||||
|
logg.debug('got recv {}'.format(str(r)))
|
||||||
|
jr = json.loads(r)
|
||||||
|
jr['id'] = self.id_seq
|
||||||
|
#return str(json.dumps(jr))
|
||||||
|
return jr
|
||||||
|
|
||||||
|
elif method == 'eth_sign':
|
||||||
|
params = PlatformMiddleware._translate_params(suspect_params)
|
||||||
|
s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0)
|
||||||
|
ipc_provider_workaround = s.connect(self.ipcaddr)
|
||||||
|
logg.info('redirecting method {} params {} original params {}'.format(method, params, suspect_params))
|
||||||
|
o = jsonrpc_request(method, params)
|
||||||
|
j = json.dumps(o)
|
||||||
|
logg.debug('send {}'.format(j))
|
||||||
|
s.send(j.encode('utf-8'))
|
||||||
|
r = s.recv(4096)
|
||||||
|
s.close()
|
||||||
|
logg.debug('got recv {}'.format(str(r)))
|
||||||
|
jr = json.loads(r)
|
||||||
|
jr['id'] = self.id_seq
|
||||||
|
return jr
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
r = self.make_request(method, suspect_params)
|
||||||
|
return r
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
cryptography==3.2.1
|
||||||
|
pysha3==1.0.2
|
||||||
|
rlp==2.0.1
|
||||||
|
json-rpc==1.13.0
|
||||||
|
confini>=0.3.6rc3,<0.5.0
|
||||||
|
coincurve==15.0.0
|
||||||
|
hexathon~=0.0.1a7
|
||||||
|
pycryptodome==3.10.1
|
||||||
|
funga>=0.5.1a1,<0.6.0
|
11
setup.cfg
Normal file
11
setup.cfg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[metadata]
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Development Status :: 3 - Alpha
|
||||||
|
Intended Audience :: Developers
|
||||||
|
Topic :: Software Development :: Libraries
|
||||||
|
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
||||||
|
license = GPLv3
|
||||||
|
license_files =
|
||||||
|
LICENSE.txt
|
61
setup.py
Normal file
61
setup.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
f = open('README.md', 'r')
|
||||||
|
long_description = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
requirements = []
|
||||||
|
f = open('requirements.txt', 'r')
|
||||||
|
while True:
|
||||||
|
l = f.readline()
|
||||||
|
if l == '':
|
||||||
|
break
|
||||||
|
requirements.append(l.rstrip())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
sql_requirements = []
|
||||||
|
f = open('sql_requirements.txt', 'r')
|
||||||
|
while True:
|
||||||
|
l = f.readline()
|
||||||
|
if l == '':
|
||||||
|
break
|
||||||
|
sql_requirements.append(l.rstrip())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
test_requirements = []
|
||||||
|
f = open('test_requirements.txt', 'r')
|
||||||
|
while True:
|
||||||
|
l = f.readline()
|
||||||
|
if l == '':
|
||||||
|
break
|
||||||
|
test_requirements.append(l.rstrip())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="funga-eth",
|
||||||
|
version="0.5.1a1",
|
||||||
|
description="Ethereum implementation of the funga keystore and signer",
|
||||||
|
author="Louis Holbrook",
|
||||||
|
author_email="dev@holbrook.no",
|
||||||
|
packages=[
|
||||||
|
'funga.eth.signer',
|
||||||
|
'funga.eth',
|
||||||
|
'funga.eth.cli',
|
||||||
|
'funga.eth.keystore',
|
||||||
|
'funga.eth.runnable',
|
||||||
|
],
|
||||||
|
install_requires=requirements,
|
||||||
|
extras_require={
|
||||||
|
'sql': sql_requirements,
|
||||||
|
},
|
||||||
|
tests_require=test_requirements,
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts': [
|
||||||
|
'funga-eth=funga.eth.runnable.signer:main',
|
||||||
|
'eth-keyfile=funga.eth.runnable.keyfile:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
url='https://gitlab.com/chaintool/funga-eth',
|
||||||
|
)
|
2
sql_requirements.txt
Normal file
2
sql_requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
psycopg2==2.8.6
|
||||||
|
sqlalchemy==1.3.20
|
0
test_requirements.txt
Normal file
0
test_requirements.txt
Normal file
88
tests/test_cli.py
Normal file
88
tests/test_cli.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# standard imports
|
||||||
|
import unittest
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from hexathon import strip_0x
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.eth.signer import EIP155Signer
|
||||||
|
from funga.eth.keystore.dict import DictKeystore
|
||||||
|
from funga.eth.cli.handle import SignRequestHandler
|
||||||
|
from funga.eth.transaction import EIP155Transaction
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
data_dir = os.path.join(script_dir, 'testdata')
|
||||||
|
|
||||||
|
class TestCli(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
#pk = bytes.fromhex('5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6')
|
||||||
|
#pk_getter = pkGetter(pk)
|
||||||
|
self.keystore = DictKeystore()
|
||||||
|
SignRequestHandler.keystore = self.keystore
|
||||||
|
self.signer = EIP155Signer(self.keystore)
|
||||||
|
SignRequestHandler.signer = self.signer
|
||||||
|
self.handler = SignRequestHandler()
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_account(self):
|
||||||
|
q = {
|
||||||
|
'id': 0,
|
||||||
|
'method': 'personal_newAccount',
|
||||||
|
'params': [''],
|
||||||
|
}
|
||||||
|
(rpc_id, result) = self.handler.process_input(q)
|
||||||
|
self.assertTrue(self.keystore.get(result))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_tx(self):
|
||||||
|
keystore_file = os.path.join(data_dir, 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72')
|
||||||
|
sender = self.keystore.import_keystore_file(keystore_file)
|
||||||
|
tx_hexs = {
|
||||||
|
'nonce': '0x',
|
||||||
|
'from': sender,
|
||||||
|
'gasPrice': "0x04a817c800",
|
||||||
|
'gas': "0x5208",
|
||||||
|
'to': '0x3535353535353535353535353535353535353535',
|
||||||
|
'value': "0x03e8",
|
||||||
|
'data': "0xdeadbeef",
|
||||||
|
'chainId': 8995,
|
||||||
|
}
|
||||||
|
tx = EIP155Transaction(tx_hexs, 42, 8995)
|
||||||
|
tx_s = tx.serialize()
|
||||||
|
|
||||||
|
# TODO: move to serialization wrapper for tests
|
||||||
|
tx_s['chainId'] = tx_s['v']
|
||||||
|
tx_s['from'] = sender
|
||||||
|
|
||||||
|
# eth_signTransaction wraps personal_signTransaction, so here we test both already
|
||||||
|
q = {
|
||||||
|
'id': 0,
|
||||||
|
'method': 'eth_signTransaction',
|
||||||
|
'params': [tx_s],
|
||||||
|
}
|
||||||
|
(rpc_id, result) = self.handler.process_input(q)
|
||||||
|
logg.debug('result {}'.format(result))
|
||||||
|
|
||||||
|
self.assertEqual(strip_0x(result), 'f86c2a8504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef82466aa0b7c1bbf52f736ada30fe253c7484176f44d6fd097a9720dc85ae5bbc7f060e54a07afee2563b0cf6d00333df51cc62b0d13c63108b2bce54ce2ad24e26ce7b4f25')
|
||||||
|
|
||||||
|
def test_sign_msg(self):
|
||||||
|
keystore_file = os.path.join(data_dir, 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72')
|
||||||
|
sender = self.keystore.import_keystore_file(keystore_file)
|
||||||
|
q = {
|
||||||
|
'id': 0,
|
||||||
|
'method': 'eth_sign',
|
||||||
|
'params': [sender, '0xdeadbeef'],
|
||||||
|
}
|
||||||
|
(rpc_id, result) = self.handler.process_input(q)
|
||||||
|
logg.debug('result msg {}'.format(result))
|
||||||
|
self.assertEqual(strip_0x(result), '50320dda75190a121b7b5979de66edadafd02bdfbe4f6d49552e79c01410d2464aae35e385c0e5b61663ff7b44ef65fa0ac7ad8a57472cf405db399b9dba3e1600')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
63
tests/test_keystore_dict.py
Normal file
63
tests/test_keystore_dict.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# standard imports
|
||||||
|
import unittest
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
from hexathon import (
|
||||||
|
strip_0x,
|
||||||
|
add_0x,
|
||||||
|
)
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.error import UnknownAccountError
|
||||||
|
from funga.eth.keystore.dict import DictKeystore
|
||||||
|
from funga.eth.signer import EIP155Signer
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
script_dir = os.path.realpath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class TestDict(unittest.TestCase):
|
||||||
|
|
||||||
|
address_hex = None
|
||||||
|
db = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.db = DictKeystore()
|
||||||
|
|
||||||
|
keystore_filepath = os.path.join(script_dir, 'testdata', 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72')
|
||||||
|
|
||||||
|
address_hex = self.db.import_keystore_file(keystore_filepath, '')
|
||||||
|
self.address_hex = add_0x(address_hex)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_key(self):
|
||||||
|
logg.debug('getting {}'.format(strip_0x(self.address_hex)))
|
||||||
|
pk = self.db.get(strip_0x(self.address_hex), '')
|
||||||
|
|
||||||
|
self.assertEqual(self.address_hex.lower(), '0x00a329c0648769a73afac7f9381e08fb43dbea72')
|
||||||
|
|
||||||
|
bogus_account = os.urandom(20).hex()
|
||||||
|
with self.assertRaises(UnknownAccountError):
|
||||||
|
self.db.get(bogus_account, '')
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_message(self):
|
||||||
|
s = EIP155Signer(self.db)
|
||||||
|
z = s.sign_ethereum_message(strip_0x(self.address_hex), b'foo')
|
||||||
|
logg.debug('zzz {}'.format(str(z)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
64
tests/test_keystore_reference.py
Normal file
64
tests/test_keystore_reference.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# standard imports
|
||||||
|
import unittest
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
# external imports
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2 import sql
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from funga.eth.keystore.sql import SQLKeystore
|
||||||
|
from funga.error import UnknownAccountError
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDatabase(unittest.TestCase):
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
cur = None
|
||||||
|
symkey = None
|
||||||
|
address_hex = None
|
||||||
|
db = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
logg.debug('setup')
|
||||||
|
# arbitrary value
|
||||||
|
symkey_hex = 'E92431CAEE69313A7BE9E443C4ABEED9BF8157E9A13553B4D5D6E7D51B5021D9'
|
||||||
|
self.symkey = bytes.fromhex(symkey_hex)
|
||||||
|
self.address_hex = '9FA61f0E52A5C51b43f0d32404625BC436bb7041'
|
||||||
|
|
||||||
|
kw = {
|
||||||
|
'symmetric_key': self.symkey,
|
||||||
|
}
|
||||||
|
self.db = SQLKeystore('postgres+psycopg2://postgres@localhost:5432/signer_test', **kw)
|
||||||
|
self.address_hex = self.db.new('foo')
|
||||||
|
#self.address_hex = add_0x(address_hex)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.db.db_session.execute('DROP INDEX ethereum_address_idx;')
|
||||||
|
self.db.db_session.execute('DROP TABLE ethereum;')
|
||||||
|
self.db.db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_key(self):
|
||||||
|
logg.debug('getting {}'.format(self.address_hex))
|
||||||
|
self.db.get(self.address_hex, 'foo')
|
||||||
|
with self.assertRaises(InvalidToken):
|
||||||
|
self.db.get(self.address_hex, 'bar')
|
||||||
|
|
||||||
|
bogus_account = '0x' + os.urandom(20).hex()
|
||||||
|
with self.assertRaises(UnknownAccountError):
|
||||||
|
self.db.get(bogus_account, 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
99
tests/test_sign.py
Normal file
99
tests/test_sign.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# standard imports
|
||||||
|
import unittest
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
from rlp import encode as rlp_encode
|
||||||
|
|
||||||
|
from funga.eth.signer import EIP155Signer
|
||||||
|
from funga.eth.transaction import EIP155Transaction
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
tx_ints = {
|
||||||
|
'nonce': 0,
|
||||||
|
'from': "0xEB014f8c8B418Db6b45774c326A0E64C78914dC0",
|
||||||
|
'gasPrice': "20000000000",
|
||||||
|
'gas': "21000",
|
||||||
|
'to': '0x3535353535353535353535353535353535353535',
|
||||||
|
'value': "1000",
|
||||||
|
'data': "deadbeef",
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_hexs = {
|
||||||
|
'nonce': '0x0',
|
||||||
|
'from': "0xEB014f8c8B418Db6b45774c326A0E64C78914dC0",
|
||||||
|
'gasPrice': "0x4a817c800",
|
||||||
|
'gas': "0x5208",
|
||||||
|
'to': '0x3535353535353535353535353535353535353535',
|
||||||
|
'value': "0x3e8",
|
||||||
|
'data': "deadbeef",
|
||||||
|
}
|
||||||
|
|
||||||
|
class pkGetter:
|
||||||
|
|
||||||
|
def __init__(self, pk):
|
||||||
|
self.pk = pk
|
||||||
|
|
||||||
|
def get(self, address, password=None):
|
||||||
|
return self.pk
|
||||||
|
|
||||||
|
|
||||||
|
class TestSign(unittest.TestCase):
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
nonce = -1
|
||||||
|
pk_getter = None
|
||||||
|
|
||||||
|
|
||||||
|
def getNonce(self):
|
||||||
|
self.nonce += 1
|
||||||
|
return self.nonce
|
||||||
|
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.pk = bytes.fromhex('5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6')
|
||||||
|
self.pk_getter = pkGetter(self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
logg.info('teardown empty')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: verify rlp tx output
|
||||||
|
def test_serialize_transaction(self):
|
||||||
|
t = EIP155Transaction(tx_ints, 0)
|
||||||
|
self.assertRegex(t.__class__.__name__, "Transaction")
|
||||||
|
s = t.serialize()
|
||||||
|
self.assertDictEqual(s, {'nonce': '0x', 'gasPrice': '0x04a817c800', 'gas': '0x5208', 'to': '0x3535353535353535353535353535353535353535', 'value': '0x03e8', 'data': '0xdeadbeef', 'v': '0x01', 'r': '0x', 's': '0x'})
|
||||||
|
r = t.rlp_serialize()
|
||||||
|
self.assertEqual(r.hex(), 'ea808504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef018080')
|
||||||
|
|
||||||
|
t = EIP155Transaction(tx_hexs, 0)
|
||||||
|
self.assertRegex(t.__class__.__name__, "Transaction")
|
||||||
|
s = t.serialize()
|
||||||
|
#o = json.loads(s)
|
||||||
|
self.assertDictEqual(s, {'nonce': '0x', 'gasPrice': '0x04a817c800', 'gas': '0x5208', 'to': '0x3535353535353535353535353535353535353535', 'value': '0x03e8', 'data': '0xdeadbeef', 'v': '0x01', 'r': '0x', 's': '0x'})
|
||||||
|
r = t.rlp_serialize()
|
||||||
|
self.assertEqual(r.hex(), 'ea808504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef018080')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_transaction(self):
|
||||||
|
t = EIP155Transaction(tx_ints, 461, 8995)
|
||||||
|
s = EIP155Signer(self.pk_getter)
|
||||||
|
z = s.sign_transaction(t)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_message(self):
|
||||||
|
s = EIP155Signer(self.pk_getter)
|
||||||
|
z = s.sign_ethereum_message(tx_ints['from'], '666f6f')
|
||||||
|
z = s.sign_ethereum_message(tx_ints['from'], b'foo')
|
||||||
|
logg.debug('zzz {}'.format(str(z)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
15
tests/test_socket.py
Normal file
15
tests/test_socket.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# standard imports
|
||||||
|
import unittest
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SocketTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_placeholder_warning(self):
|
||||||
|
logg.warning('socket tests are missing! :/')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user