web3.py/web3/utils/transactions.py
2017-05-18 18:26:03 -04:00

264 lines
8.4 KiB
Python
Executable File

import random
from ..rlp import encode, Serializable, decode
from ..rlp.sedes import big_endian_int, binary, Binary
from ..rlp.utils import int_to_big_endian
from ..eth_utils import (
decode_hex,
force_bytes,
coerce_args_to_bytes,
is_string,
to_canonical_address,
to_normalized_address,
pad_left,
keccak,
)
from .encoding import (
to_decimal,
decode_big_endian_int,
)
from .compat import (
Timeout,
)
def wait_for_transaction_receipt(web3, txn_hash, timeout=120):
with Timeout(timeout) as _timeout:
while True:
txn_receipt = web3.eth.getTransactionReceipt(txn_hash)
if txn_receipt is not None:
break
_timeout.sleep(random.random())
return txn_receipt
def get_block_gas_limit(web3, block_identifier=None):
if block_identifier is None:
block_identifier = web3.eth.blockNumber
block = web3.eth.getBlock(block_identifier)
return block['gasLimit']
def get_buffered_gas_estimate(web3, transaction, gas_buffer=100000):
gas_estimate_transaction = dict(**transaction)
gas_estimate = web3.eth.estimateGas(gas_estimate_transaction)
gas_limit = get_block_gas_limit(web3)
if gas_estimate > gas_limit:
raise ValueError(
"Contract does not appear to be delpoyable within the "
"current network gas limits. Estimated: {0}. Current gas "
"limit: {1}".format(gas_estimate, gas_limit)
)
return min(gas_limit, gas_estimate + gas_buffer)
def is_secp256k1_available():
try:
import secp256k1 # noqa
except ImportError:
return False
else:
return True
def is_bitcoin_available():
try:
import bitcoin # noqa
except ImportError:
return False
else:
return True
# in the yellow paper it is specified that s should be smaller than secpk1n (eq.205)
secpk1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
TT256 = 2 ** 256
address_sedes = Binary.fixed_length(20, allow_empty=True)
class Transaction(Serializable):
"""
# Derived from `pyethereum.transaction.Transaction`
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', address_sedes),
('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 = to_canonical_address(to)
assert len(to) == 20 or len(to) == 0
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 ValueError("Values way too high!")
@property
def sender(self):
if not self._sender:
if not is_bitcoin_available() or not is_secp256k1_available():
raise ImportError(
"In order to derive the sender for transactions the "
"`bitcoin` and `secp256k1` packages must be installed."
)
from bitcoin import N
from secp256k1 import PublicKey, ALL_FLAGS
# Determine sender
if self.v:
has_invalid_signature_values = (
self.r >= N or
self.s >= N or
self.v < 27 or
self.v > 28 or
self.r == 0 or
self.s == 0
)
if has_invalid_signature_values:
raise ValueError("Invalid signature values!")
rlpdata = encode(self, UnsignedTransaction)
rawhash = keccak(rlpdata)
pk = PublicKey(flags=ALL_FLAGS)
try:
pk.public_key = pk.ecdsa_recover(
rawhash,
pk.ecdsa_recoverable_deserialize(
pad_left(
int_to_big_endian(self.r),
32,
b'\x00',
) + pad_left(
int_to_big_endian(self.s),
32,
b'\x00',
),
self.v - 27
),
raw=True
)
pub = pk.serialize(compressed=False)
except Exception:
raise ValueError("Invalid signature values (x^3+7 is non-residue)")
if pub[1:] == b"\x00" * (len(pub) - 1):
raise ValueError("Invalid signature (zero privkey cannot sign)")
self._sender = to_normalized_address(keccak(pub[1:])[-20:])
assert self.sender == self._sender
else:
self._sender = 0
return self._sender
@sender.setter
def sender(self, value):
self._sender = value
@coerce_args_to_bytes
def sign(self, key):
"""Sign this transaction with a private key.
A potentially already existing signature would be overridden.
"""
if not is_bitcoin_available() or not is_secp256k1_available():
raise ImportError(
"In order to sign transactions the "
"`bitcoin` and `secp256k1` packages must be installed."
)
from bitcoin import privtopub
from secp256k1 import PrivateKey
if key in (0, b'', b'\x00' * 32, b'0' * 64):
raise ValueError("Zero privkey cannot sign")
rawhash = keccak(encode(self, UnsignedTransaction))
if len(key) in {64, 66}:
# we need a binary key
key = decode_hex(key)
pk = PrivateKey(key, raw=True)
sig_bytes, rec_id = pk.ecdsa_recoverable_serialize(
pk.ecdsa_sign_recoverable(rawhash, raw=True)
)
signature = sig_bytes + force_bytes(chr(rec_id))
self.v = (ord(signature[64]) if is_string(signature[64]) else signature[64]) + 27
self.r = decode_big_endian_int(signature[0:32])
self.s = decode_big_endian_int(signature[32:64])
self.sender = to_normalized_address(keccak(privtopub(key)[1:])[-20:])
return self
UnsignedTransaction = Transaction.exclude(['v', 'r', 's'])
def serialize_transaction(transaction):
unsigned_transaction = UnsignedTransaction(
nonce=to_decimal(transaction['nonce']),
gasprice=to_decimal(transaction['gasPrice']),
startgas=to_decimal(transaction['gas']),
to=transaction['to'],
value=to_decimal(transaction['value']),
data=decode_hex(transaction['data']),
)
return encode(unsigned_transaction, UnsignedTransaction)
def add_signature_to_transaction(serialize_transaction, signature):
unsigned_transaction = decode(serialize_transaction, UnsignedTransaction)
v = (ord(signature[64]) if is_string(signature[64]) else signature[64]) + 27
r = decode_big_endian_int(signature[0:32])
s = decode_big_endian_int(signature[32:64])
signed_transaction = Transaction(
nonce=unsigned_transaction.nonce,
gasprice=unsigned_transaction.gasprice,
startgas=unsigned_transaction.startgas,
to=unsigned_transaction.to,
value=unsigned_transaction.value,
data=unsigned_transaction.data,
v=v,
r=r,
s=s,
)
return signed_transaction