Last active
August 14, 2016 22:03
-
-
Save AlexeyAkhunov/b3e9e57b8138428133dd83fff325f4ba to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# In order for this to work, you need to install parity (of version at least 1.3.0) | |
# and run it like this: | |
# | |
# parity --tracing on --pruning=archive | |
# | |
# Allow it to sync to at least the hard fork block before running this script | |
# | |
import httplib | |
import json | |
import sys | |
# This is part of output of command line: | |
# solc --hashes DAO.sol, where DAO.sol is the source code of theDAO | |
method_hashes_tex = ''' | |
23b872dd: transferFrom(address,address,uint256) | |
4b6753bc: closingTime() | |
4e10c3ee: transferWithoutReward(address,uint256) | |
70a08231: balanceOf(address) | |
82661dc4: splitDAO(uint256,address) | |
a9059cbb: transfer(address,uint256) | |
be7c29c1: getNewDAOAddress(uint256) | |
dbde1988: transferFromWithoutReward(address,address,uint256) | |
''' | |
method_hashes = {} # To look up the method signature based on the ABI | |
valid_hashes = set() # To check if there is any method with this signature (to detect calls to fallback) | |
for line in method_hashes_tex.splitlines(): | |
parts = line.split(': ') | |
if len(parts) > 1: | |
method_hashes[parts[1]] = parts[0] | |
valid_hashes.add(parts[0]) | |
THE_DAO_ADDRESS = 'bb9bc244d798123fde783fcc1c72d3bb8c189413' | |
HARDFORK_BLOCK = 1920000 | |
class Transfer: | |
""" Represents one transfer of DAO tokens (via transfer function) """ | |
def __init__(self, transaction_hash_, source_address_, target_address_, tokens_): | |
self.transaction_hash = transaction_hash_ | |
self.source_address = source_address_ | |
self.target_address = target_address_ | |
self.tokens = tokens_ | |
def get_dao_creation_block(connection, dao_address): | |
""" Uses binary search to find the block number at which the dao has been created """ | |
params = [{"to": "0x" + dao_address, "data": "0x" + method_hashes['closingTime()']}] | |
http.request( | |
method='POST', | |
url='', | |
body=json.dumps({"jsonrpc": "2.0", "method": "eth_call", "params": params, "id": 0}), | |
headers={'Content-Type': 'application/json'}) | |
response = http.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read childDAO closing time', response.status, response.reason | |
sys.exit(0) | |
closing_time = long(json.load(response)['result'][2:], 16) | |
low_block_number = 1 | |
high_block_number = HARDFORK_BLOCK | |
while high_block_number - low_block_number > 1: | |
med_block_number = (low_block_number + high_block_number) / 2 | |
params = [str(med_block_number), "false"] | |
http.request( | |
method='POST', | |
url='', | |
body=json.dumps({"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": params, "id": 0}), | |
headers={'Content-Type': 'application/json'}) | |
response = http.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read block information', response.status, response.reason | |
sys.exit(0) | |
timestamp = long(json.load(response)['result']['timestamp'][2:], 16) | |
if timestamp < closing_time: | |
low_block_number = med_block_number | |
else: | |
high_block_number = med_block_number | |
return low_block_number | |
# Open connection to parity JSON RPC | |
http = httplib.HTTPConnection('localhost:8545') | |
DAO_CREATION_BLOCK = get_dao_creation_block(http, THE_DAO_ADDRESS) | |
def traces_to_address(connection, to_address): | |
""" Reads trace for an address from given HTTP connection """ | |
params = [{ | |
"fromBlock": str(DAO_CREATION_BLOCK), # Block number where DAO was created | |
"toBlock": str(HARDFORK_BLOCK), # Block number of the hard-fork | |
}] | |
params[0]['toAddress'] = [to_address] | |
connection.request( | |
method='POST', | |
url='', | |
body=json.dumps({"jsonrpc": "2.0", "method": "trace_filter", "params": params, "id": 0}), | |
headers={'Content-Type': 'application/json'}) | |
response = connection.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read to theDAO trace', response.status, response.reason | |
sys.exit(0) | |
print 'Parsing JSON...' | |
return json.load(response) | |
# Get transactions sent to theDAO (internal or external) | |
print 'Reading transactions sent to theDAO via Parity JSON RPC...' | |
to_dao = traces_to_address(http, '0x' + THE_DAO_ADDRESS) | |
to_dao_result = to_dao['result'] # Array of all transactions to theDAO (internal and external) | |
transfer_list = [] | |
all_addresses = set() # Build up the list of all addresses involved in withdraws, splits, and transfers | |
proposal_id_by_address = {} | |
all_transactions = set() # Build up the list of all transaction hashes, for re-tracing | |
for r in to_dao_result: | |
result = r['result'] | |
if 'failedCall' in result: | |
# Filter out failed transactions | |
continue | |
action = r['action'] | |
if 'call' not in action: | |
# Filter out 'create' actions | |
continue | |
call = action['call'] | |
# Data sent with the transaction | |
call_input = call['input'] | |
# First 2 characters are '0x', and we extract 20 bytes of address (40 hex digits) | |
from_address = str(call['from'][2:2+40]) | |
transaction = r['transactionHash'] | |
# First 2 characters are '0x', then there are 8 hex digits (4 bytes) of method signature | |
signature = str(call_input[2:2+8]) | |
if signature == method_hashes['transfer(address,uint256)'] or \ | |
signature == method_hashes['transferWithoutReward(address,uint256)']: | |
output = long(result['call']['output'], 16) | |
if output == 1: | |
# First 2 characters are '0x', and we extract 20 bytes of address (40 hex digits) | |
# First argument of transfer is the target_address (20 bytes, 40 hex digits), | |
# pre-pended by 12 0-bytes (24 0 hex digits) | |
target_address = str(call_input[34:74]) | |
# 32 bytes (64 hex digits) of the value | |
tokens = long(call_input[74:138], 16) | |
all_addresses.add(from_address) | |
all_addresses.add(target_address) | |
transfer_list.append(Transfer(transaction_hash_=transaction, source_address_=from_address, target_address_=target_address, tokens_=tokens)) | |
all_transactions.add(transaction) | |
elif signature == method_hashes['transferFrom(address,address,uint256)'] or \ | |
signature == method_hashes['transferFromWithoutReward(address,address,uint256)']: | |
output = long(result['call']['output'], 16) | |
if output == 1: | |
# First 2 characters are '0x', and we extract 20 bytes of address (40 hex digits) | |
# First argument of transferFrom is the source_address (20 bytes, 40 hex digits), | |
# pre-pended by 12 0-bytes (24 0 hex digits) | |
source_address = str(call_input[34:74]) | |
# Second argument is the target_address (20 bytes, 40 hex digits), | |
# pre-pended by 12 0-bytes (24 0 hex digits) | |
target_address = str(call_input[98:138]) | |
# 32 bytes (64 hex digits) of the value | |
tokens = long(call_input[138:138+64], 16) | |
all_addresses.add(source_address) | |
all_addresses.add(target_address) | |
transfer_list.append(Transfer(transaction_hash_=transaction, source_address_=source_address, target_address_=target_address, tokens_=tokens)) | |
all_transactions.add(transaction) | |
elif signature == method_hashes['splitDAO(uint256,address)']: | |
# We only need to analyse splitDAO calls to | |
# construct the map of addresses that might have some tokens in childDAOs | |
proposal_id = int(call_input[10:10+64], 16) | |
all_addresses.add(from_address) | |
if from_address not in proposal_id_by_address: | |
proposal_id_by_address[from_address] = set() | |
proposal_id_by_address[from_address].add(proposal_id) | |
def retrace_transactions(connection, transaction): | |
""" Reads traces for given list of transactions """ | |
connection.request( | |
method='POST', | |
url='', | |
body='{"jsonrpc": "2.0", "method": "trace_transaction", "params": ["' + str(transaction) + '"], "id": 0}', | |
headers={'Content-Type': 'application/json'}) | |
response = connection.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read traces of transactions', response.status, response.reason | |
sys.exit(0) | |
return json.load(response) | |
# Retrace all the transaction to see if they failed | |
# This needs to be done because sometimes the 'failedCall' is not present in all the internal | |
# transactions, even though the parent transaction failed | |
print 'Retracing all transactions via Parity JSON RPC...' | |
transactions_done = 0 | |
failed_transactions = set() # Store failed transaction here, and later use to filter out failed transfers | |
for transaction in all_transactions: | |
retrace = retrace_transactions(http, transaction) | |
for r in retrace['result']: | |
if 'failedCall' in r['result']: | |
failed_transactions.add(transaction) | |
transactions_done += 1 | |
if transactions_done % 1000 == 0: | |
print 'Retraced %d out of %d transaction' % (transactions_done, len(all_transactions)) | |
print 'Failed transactions found during the retrace: %d' % len(failed_transactions) | |
# Filter out transfers in the failed transactions | |
transfer_list = [transfer for transfer in transfer_list if transfer.transaction_hash not in failed_transactions] | |
from itertools import groupby | |
# Aggregate transfers by source_address | |
transfer_by_source = {} | |
transfer_list.sort(key=lambda transfer: transfer.source_address) | |
for source_address, group in groupby(transfer_list, lambda transfer: transfer.source_address): | |
total_transfer = sum(map(lambda transfer: transfer.tokens, group)) | |
transfer_by_source[source_address] = total_transfer | |
# Aggregate transfers by source_address | |
transfer_by_target = {} | |
transfer_list.sort(key=lambda transfer: transfer.target_address) | |
for target_address, group in groupby(transfer_list, lambda transfer: transfer.target_address): | |
total_transfer = sum(map(lambda transfer: transfer.tokens, group)) | |
transfer_by_target[target_address] = total_transfer | |
def get_child_dao_address(connection, proposal_id): | |
""" Reads address of childDAO given proposal_id """ | |
params = [{ | |
"to": "0x" + THE_DAO_ADDRESS, # DAO address | |
"data": "0x" + method_hashes['getNewDAOAddress(uint256)'] + format(proposal_id, '064x') | |
}, "%d" % HARDFORK_BLOCK] | |
http.request( | |
method='POST', | |
url='', | |
body=json.dumps({"jsonrpc": "2.0", "method": "eth_call", "params": params, "id": 0}), | |
headers={'Content-Type': 'application/json'}) | |
response = http.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read childDAO address', response.status, response.reason | |
sys.exit(0) | |
return json.load(response)['result'][26:] | |
child_dao_addresses = {} | |
child_dao_creation_blocks = {} | |
for proposal_id in range(1, 110): | |
child_dao_address = str(get_child_dao_address(http, proposal_id)) | |
if child_dao_address != '' and child_dao_address != '0'*40: | |
# Find block number at which the childDAO got created | |
creation_block = get_dao_creation_block(http, child_dao_address) | |
child_dao_addresses[proposal_id] = child_dao_address | |
child_dao_creation_blocks[proposal_id] = creation_block | |
# For each address, request DAO token balance at the time of DAO creation | |
# and at the time of the hard fork | |
def read_dao_balance(connection, dao_address, holder_address, block_number): | |
""" Reads DAO balance for an address at given block from given HTTP connection """ | |
params = [{ | |
"to": "0x" + dao_address, # DAO address | |
"data": "0x" + method_hashes['balanceOf(address)'] + '0'*24 + holder_address | |
}] | |
connection.request( | |
method='POST', | |
url='', | |
body=json.dumps({"jsonrpc": "2.0", "method": "eth_call", "params": params + [str(block_number)], "id": 0}), | |
headers={'Content-Type': 'application/json'}) | |
response = connection.getresponse() | |
if response.status != httplib.OK: | |
print 'Could not read to theDAO balance', response.status, response.reason | |
sys.exit(0) | |
return int(json.load(response)['result'][2:], 16) | |
class AddressInfo: | |
""" Stores information about one address to be grouped by proposal_id and printed """ | |
def __init__(self, address_, tokens_burnt_, child_tokens_): | |
self.address = address_ | |
self.tokens_burnt = tokens_burnt_ | |
self.child_tokens = child_tokens_ | |
address_infos_by_proposal = {} | |
addresses_done = 0 | |
for a in all_addresses: | |
balance_at_creation = read_dao_balance(http, THE_DAO_ADDRESS, a, DAO_CREATION_BLOCK) | |
balance_at_hardfork = read_dao_balance(http, THE_DAO_ADDRESS, a, HARDFORK_BLOCK) | |
transferred_from_a = transfer_by_source[a] if a in transfer_by_source else 0 | |
transferred_to_a = transfer_by_target[a] if a in transfer_by_target else 0 | |
tokens_burnt = balance_at_creation + transferred_to_a - transferred_from_a - balance_at_hardfork | |
child_tokens = {} | |
total_child_tokens = 0 | |
if a in proposal_id_by_address: | |
for proposal_id in proposal_id_by_address[a]: | |
child_dao_address = child_dao_addresses[proposal_id] | |
child_dao_creation_block = child_dao_creation_blocks[proposal_id] | |
child_dao_balance_at_hardfork = read_dao_balance(http, child_dao_address, a, child_dao_creation_block) | |
if child_dao_balance_at_hardfork > 0: | |
child_tokens[proposal_id] = child_dao_balance_at_hardfork | |
total_child_tokens += child_dao_balance_at_hardfork | |
if a != '0'*40 and (len(child_tokens) > 0 or tokens_burnt != 0): | |
for proposal_id in child_tokens: | |
if proposal_id not in address_infos_by_proposal: | |
address_infos_by_proposal[proposal_id] = [] | |
address_infos_by_proposal[proposal_id].append(AddressInfo(address_=a, tokens_burnt_=tokens_burnt, child_tokens_=child_tokens)) | |
addresses_done += 1 | |
if addresses_done % 1000 == 0: | |
print 'Checked %d out of %d addresses' % (addresses_done, len(all_addresses)) | |
for proposal_id in sorted(address_infos_by_proposal.keys()): | |
print '=================================================' | |
print 'Proposal #%d' % proposal_id | |
print '-------------------------------------------------' | |
for address_info in address_infos_by_proposal[proposal_id]: | |
total_child_tokens = sum((tokens for p, tokens in address_info.child_tokens.iteritems() if p == proposal_id)) | |
print address_info.address, 'burnt:', address_info.tokens_burnt, 'childTokens:', address_info.child_tokens, \ | |
'ratio', 'inf' if address_info.tokens_burnt == 0 else float(total_child_tokens) / float(address_info.tokens_burnt) | |
http.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment