Skip to content

Commit

Permalink
Remove dependencies on pysha3 and ethereum modules
Browse files Browse the repository at this point in the history
- pysha3: Monero and Ethereum use the old pre-SHA3 version of the keccak
  hash function, which is not supported by hashlib.sha3.

  The pysha3 package supports the function but is not portable.

  Therefore, use the pure-Python implementation mmgen.keccak as a fallback,
  making the pysha3 package optional.

  Use of mmgen.keccak may be forced with --use-internal-keccak-module

  mmgen.keccak was ported from the Python 2 implementation at
  https://github.com/ctz/keccak

- ethereum (pyethereum): Installation of this package presents numerous
  problems due to poor maintenance and many superfluous dependencies.

  Therefore, use stripped-down and modified local versions of
  ethereum.transactions and ethereum.utils as fallbacks, making the ethereum
  package optional.

  The local pyethereum.utils uses mmgen.keccak as a fallback for pysha3's
  keccak implementation.
  • Loading branch information
mmgen committed Mar 23, 2019
1 parent 281e1f3 commit a7126ed
Show file tree
Hide file tree
Showing 17 changed files with 1,083 additions and 74 deletions.
71 changes: 50 additions & 21 deletions mmgen/addr.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,27 @@ def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Segwit redeem script not supported by this address type')

class AddrGeneratorEthereum(AddrGenerator):

def __init__(self,addr_type):

try:
assert not g.use_internal_keccak_module
from sha3 import keccak_256
except:
from mmgen.keccak import keccak_256
self.keccak_256 = keccak_256

from mmgen.protocol import hash256
self.hash256 = hash256

return AddrGenerator.__init__(addr_type)

def to_addr(self,pubhex):
assert type(pubhex) == PubKey
import sha3
return CoinAddr(sha3.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
return CoinAddr(self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])

def to_wallet_passwd(self,sk_hex):
from mmgen.protocol import hash256
return WalletPassword(hash256(sk_hex)[:32])
return WalletPassword(self.hash256(sk_hex)[:32])

def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Segwit redeem script not supported by this address type')
Expand Down Expand Up @@ -132,6 +145,31 @@ def to_segwit_redeem_script(self,pubhex):

class AddrGeneratorMonero(AddrGenerator):

def __init__(self,addr_type):

try:
assert not g.use_internal_keccak_module
from sha3 import keccak_256
except:
from mmgen.keccak import keccak_256
self.keccak_256 = keccak_256

from mmgen.protocol import hash256
self.hash256 = hash256

if opt.use_old_ed25519:
from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
else:
from mmgen.ed25519ll_djbec import scalarmult
from mmgen.ed25519 import edwards,encodepoint,B

self.edwards = edwards
self.encodepoint = encodepoint
self.scalarmult = scalarmult
self.B = B

return AddrGenerator.__init__(addr_type)

def b58enc(self,addr_bytes):
enc = baseconv.fromhex
l = len(addr_bytes)
Expand All @@ -141,42 +179,33 @@ def b58enc(self,addr_bytes):

def to_addr(self,sk_hex): # sk_hex instead of pubhex

if opt.use_old_ed25519:
from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
else:
from mmgen.ed25519ll_djbec import scalarmult
from mmgen.ed25519 import edwards,encodepoint,B

# Source and license for scalarmultbase function:
# https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
# Copyright (c) 2014-2016, The Monero Project
# All rights reserved.
def scalarmultbase(e):
if e == 0: return [0, 1]
Q = scalarmult(B, e//2)
Q = edwards(Q, Q)
if e & 1: Q = edwards(Q, B)
Q = self.scalarmult(self.B, e//2)
Q = self.edwards(Q, Q)
if e & 1: Q = self.edwards(Q, self.B)
return Q

def hex2int_le(hexstr):
return int((bytes.fromhex(hexstr)[::-1]).hex(),16)

vk_hex = self.to_viewkey(sk_hex)
pk_str = encodepoint(scalarmultbase(hex2int_le(sk_hex)))
pvk_str = encodepoint(scalarmultbase(hex2int_le(vk_hex)))
pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
addr_p1 = bytes.fromhex(g.proto.addr_ver_num['monero'][0]) + pk_str + pvk_str

import sha3
return CoinAddr(self.b58enc(addr_p1 + sha3.keccak_256(addr_p1).digest()[:4]))
return CoinAddr(self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]))

def to_wallet_passwd(self,sk_hex):
from mmgen.protocol import hash256
return WalletPassword(hash256(sk_hex)[:32])
return WalletPassword(self.hash256(sk_hex)[:32])

def to_viewkey(self,sk_hex):
assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex))
import sha3
return MoneroViewKey(g.proto.preprocess_key(sha3.keccak_256(bytes.fromhex(sk_hex)).hexdigest(),None))
return MoneroViewKey(g.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).hexdigest(),None))

def to_segwit_redeem_script(self,sk_hex):
raise NotImplementedError('Monero addresses incompatible with Segwit')
Expand Down
14 changes: 11 additions & 3 deletions mmgen/altcoins/eth/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@
altcoins.eth.contract: Ethereum contract and token classes for the MMGen suite
"""

from sha3 import keccak_256
from decimal import Decimal
import rlp

from mmgen.globalvars import g
from mmgen.common import *
from mmgen.obj import MMGenObject,TokenAddr,CoinTxID,ETHAmt
from mmgen.util import msg,msg_r,pmsg,pdie
from mmgen.util import msg,pmsg

try:
assert not g.use_internal_keccak_module
from sha3 import keccak_256
except:
from mmgen.keccak import keccak_256

def parse_abi(s):
return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)]
Expand Down Expand Up @@ -103,7 +108,10 @@ def txcreate( self,from_addr,to_addr,amt,start_gas,gasPrice,nonce=None,
'data': bytes.fromhex(data) }

def txsign(self,tx_in,key,from_addr,chain_id=None):
from ethereum.transactions import Transaction

try: from ethereum.transactions import Transaction
except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction

if chain_id is None:
chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpch.caps]
chain_id = int(g.rpch.request(chain_id_method),16)
Expand Down
Empty file.
215 changes: 215 additions & 0 deletions mmgen/altcoins/eth/pyethereum/transactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#
# Adapted from: # https://github.com/ethereum/pyethereum/blob/master/ethereum/transactions.py
#
import rlp
from rlp.sedes import big_endian_int,binary
from mmgen.altcoins.eth.pyethereum.utils import (
str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,TT256,mk_contract_address,
ecsign,ecrecover_to_pub,normalize_key )
import mmgen.altcoins.eth.pyethereum.utils as utils

class InvalidTransaction(Exception): pass
class opcodes(object):
GTXCOST = 21000 # TX BASE GAS COST
GTXDATAZERO = 4 # TX DATA ZERO BYTE GAS COST
GTXDATANONZERO = 68 # TX DATA NON ZERO BYTE GAS COST

# in the yellow paper it is specified that s should be smaller than
# secpk1n (eq.205)
secpk1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
null_address = b'\xff' * 20

class Transaction(rlp.Serializable):

"""
A transaction is stored as:
[nonce, gasprice, startgas, to, value, data, v, r, s]
nonce is the number of transactions already sent by that account, encoded
in binary form (eg. 0 -> '', 7 -> '\x07', 1000 -> '\x03\xd8').
(v,r,s) is the raw Electrum-style signature of the transaction without the
signature made with the private key corresponding to the sending account,
with 0 <= v <= 3. From an Electrum-style signature (65 bytes) it is
possible to extract the public key, and thereby the address, directly.
A valid transaction is one where:
(i) the signature is well-formed (ie. 0 <= v <= 3, 0 <= r < P, 0 <= s < N,
0 <= r < P - N if v >= 2), and
(ii) the sending account has enough funds to pay the fee and the value.
"""

fields = [
('nonce', big_endian_int),
('gasprice', big_endian_int),
('startgas', big_endian_int),
('to', utils.address),
('value', big_endian_int),
('data', binary),
('v', big_endian_int),
('r', big_endian_int),
('s', big_endian_int),
]

_sender = None

def __init__(self, nonce, gasprice, startgas, to, value, data, v=0, r=0, s=0):
# self.data = None

to = utils.normalize_address(to, allow_blank=True)

super(
Transaction,
self).__init__(
nonce,
gasprice,
startgas,
to,
value,
data,
v,
r,
s)

if self.gasprice >= TT256 or self.startgas >= TT256 or \
self.value >= TT256 or self.nonce >= TT256:
raise InvalidTransaction("Values way too high!")

@property
def sender(self):
if not self._sender:
# Determine sender
if self.r == 0 and self.s == 0:
self._sender = null_address
else:
if self.v in (27, 28):
vee = self.v
sighash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))

elif self.v >= 37:
vee = self.v - self.network_id * 2 - 8
assert vee in (27, 28)
rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [self.network_id, '', ''])
sighash = utils.sha3(rlpdata)
else:
raise InvalidTransaction("Invalid V value")
if self.r >= secpk1n or self.s >= secpk1n or self.r == 0 or self.s == 0:
raise InvalidTransaction("Invalid signature values!")
pub = ecrecover_to_pub(sighash, vee, self.r, self.s)
if pub == b'\x00' * 64:
raise InvalidTransaction(
"Invalid signature (zero privkey cannot sign)")
self._sender = utils.sha3(pub)[-20:]
return self._sender

@property
def network_id(self):
if self.r == 0 and self.s == 0:
return self.v
elif self.v in (27, 28):
return None
else:
return ((self.v - 1) // 2) - 17

@sender.setter
def sender(self, value):
self._sender = value

def sign(self, key, network_id=None):
"""Sign this transaction with a private key.
A potentially already existing signature would be overridden.
"""
if network_id is None:
rawhash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))
else:
assert 1 <= network_id < 2**63 - 18
rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [network_id, b'', b''])
rawhash = utils.sha3(rlpdata)

key = normalize_key(key)

v, r, s = ecsign(rawhash, key)
if network_id is not None:
v += 8 + network_id * 2

ret = self.copy(
v=v, r=r, s=s
)
ret._sender = utils.privtoaddr(key)
return ret

@property
def hash(self):
return utils.sha3(rlp.encode(self))

def to_dict(self):
d = {}
for name, _ in self.__class__._meta.fields:
d[name] = getattr(self, name)
if name in ('to', 'data'):
d[name] = '0x' + encode_hex(d[name])
d['sender'] = '0x' + encode_hex(self.sender)
d['hash'] = '0x' + encode_hex(self.hash)
return d

@property
def intrinsic_gas_used(self):
num_zero_bytes = str_to_bytes(self.data).count(ascii_chr(0))
num_non_zero_bytes = len(self.data) - num_zero_bytes
return (opcodes.GTXCOST
# + (0 if self.to else opcodes.CREATE[3])
+ opcodes.GTXDATAZERO * num_zero_bytes
+ opcodes.GTXDATANONZERO * num_non_zero_bytes)

@property
def creates(self):
"returns the address of a contract created by this tx"
if self.to in (b'', '\0' * 20):
return mk_contract_address(self.sender, self.nonce)

def __eq__(self, other):
return isinstance(other, self.__class__) and self.hash == other.hash

def __lt__(self, other):
return isinstance(other, self.__class__) and self.hash < other.hash

def __hash__(self):
return utils.big_endian_to_int(self.hash)

def __ne__(self, other):
return not self.__eq__(other)

def __repr__(self):
return '<Transaction(%s)>' % encode_hex(self.hash)[:4]

def __structlog__(self):
return encode_hex(self.hash)

# This method should be called for block numbers >= HOMESTEAD_FORK_BLKNUM only.
# The >= operator is replaced by > because the integer division N/2 always produces the value
# which is by 0.5 less than the real N/2
def check_low_s_metropolis(self):
if self.s > secpk1n // 2:
raise InvalidTransaction("Invalid signature S value!")

def check_low_s_homestead(self):
if self.s > secpk1n // 2 or self.s == 0:
raise InvalidTransaction("Invalid signature S value!")


class UnsignedTransaction(rlp.Serializable):
fields = [
(field, sedes) for field, sedes in Transaction._meta.fields
if field not in "vrs"
]

def unsigned_tx_from_tx(tx):
return UnsignedTransaction(
nonce=tx.nonce,
gasprice=tx.gasprice,
startgas=tx.startgas,
to=tx.to,
value=tx.value,
data=tx.data,
)

0 comments on commit a7126ed

Please sign in to comment.