├── .gitignore ├── README.md ├── examples ├── datafeed.cll ├── datafeed.py ├── decentralized-dropbox.cll ├── egalitarian-dao.cll ├── escrow.cll ├── escrow.py ├── fountain.cll ├── fountain.py ├── hedging.cll ├── hedging.py ├── i_want_half.cll ├── i_want_half.py ├── lockin-escrow.cll ├── lockin-escrow.py ├── namecoin.cll ├── namecoin.py ├── subcurrency.cll └── subcurrency.py ├── lib ├── __init__.py └── sim.py ├── run.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | .tox/ 29 | .coverage 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Rope 43 | .ropeproject 44 | 45 | # Django stuff: 46 | *.log 47 | *.pot 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | 52 | # Swap files 53 | *.swp 54 | *.swo 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum CLL Contract Simulator 2 | 3 | ## Description 4 | 5 | Simulator of the Ethereum C-Like Language Contracts. The intention of this 6 | simulator is to help contract coders develop and test their work using test 7 | driven best practises and in full isolation, without the need to connect to the 8 | Ethereum Test Net. 9 | 10 | Contracts from the whitepaper are slightly modified to make them Python syntax 11 | compatible and contained within a `Contract` class. Automated testing scenarios 12 | are also written in a `Simulator` DSL (similar to unit tests) which can run, 13 | inspect and verify the outcome of contract runs. 14 | 15 | ## Working Examples 16 | 17 | | Name | Original file | Simulation | 18 | | --------------- | --------------------------------------------------- | ----------------------------------------- | 19 | | SubCurrency | [subcurrency.cll](examples/subcurrency.cll) | [subcurrency.py](examples/subcurrency.py) | 20 | | Namecoin | [namecoin.cll](examples/namecoin.cll) | [namecoin.py](examples/namecoin.py) | 21 | | Datafeed | [datafeed.cll](examples/datafeed.cll) | [datafeed.py](examples/datafeed.py) | 22 | | Hedging | [hedging.cll](examples/hedging.cll) | [hedging.py](examples/hedging.py) | 23 | | Fountain | [fountain.cll](examples/fountain.cll) | [fountain.py](examples/fountain.py) | 24 | | Egalitarian DAO | [egalitarian-dao.cll](examples/egalitarian-dao.cll) | | 25 | 26 | 27 | ## Usage 28 | 29 | `./run.py examples/subcurrency.py` 30 | 31 | This will execute several simulation scenarios on the Sub-Currency example from the Ethereum whitepaper. 32 | 33 | ### Output 34 | 35 | ``` 36 | sim INFO RUN test_insufficient_fee: 37 | sim WARNING Stopped: Insufficient fee 38 | sim INFO -------------------- 39 | sim INFO RUN test_creation: 40 | sim DEBUG Accessing storage 1000 41 | subcurrency INFO Initializing storage for creator alice 42 | sim DEBUG Setting storage alice to 1000000000000000000 43 | sim DEBUG Setting storage 1000 to 1 44 | sim INFO -------------------- 45 | sim INFO RUN test_alice_to_bob: 46 | sim DEBUG Accessing storage 1000 47 | sim DEBUG Accessing storage alice 48 | subcurrency INFO Transfering 1000 from alice to bob 49 | sim DEBUG Accessing storage alice 50 | sim DEBUG Setting storage alice to 999999999999999000 51 | sim DEBUG Accessing storage bob 52 | sim DEBUG Setting storage bob to 1000 53 | sim INFO -------------------- 54 | sim INFO RUN test_bob_to_charlie_invalid: 55 | sim DEBUG Accessing storage 1000 56 | sim DEBUG Accessing storage bob 57 | sim DEBUG Accessing storage bob 58 | sim WARNING Stopped: Insufficient funds, bob has 1000 needs 1001 59 | sim INFO -------------------- 60 | sim INFO RUN test_bob_to_charlie_valid: 61 | sim DEBUG Accessing storage 1000 62 | sim DEBUG Accessing storage bob 63 | subcurrency INFO Transfering 1000 from bob to charlie 64 | sim DEBUG Accessing storage bob 65 | sim DEBUG Setting storage bob to 0 66 | sim DEBUG Accessing storage charlie 67 | sim DEBUG Setting storage charlie to 1000 68 | sim INFO -------------------- 69 | subcurrency INFO , {1000: 1, 'charlie': 1000, 'bob': 0, 'alice': 999999999999999000})> 70 | ``` 71 | 72 | ## License 73 | 74 | Released under the MIT License. 75 | -------------------------------------------------------------------------------- /examples/datafeed.cll: -------------------------------------------------------------------------------- 1 | if tx.sender != FEEDOWNER: 2 | stop 3 | contract.storage[data[0]] = data[1] 4 | -------------------------------------------------------------------------------- /examples/datafeed.py: -------------------------------------------------------------------------------- 1 | from sim import Contract, Tx, Simulation, stop 2 | 3 | class DataFeed(Contract): 4 | """DataFeed contract example from https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-White-Paper#wiki-financial-derivatives""" 5 | 6 | def run(self, tx, contract, block): 7 | if tx.sender != FEEDOWNER: 8 | stop('Sender is not feed owner') 9 | contract.storage[tx.data[0]] = tx.data[1] 10 | 11 | 12 | class DataFeedRun(Simulation): 13 | 14 | contract = DataFeed(FEEDOWNER='alice') 15 | 16 | def test_invalid_sender(self): 17 | tx = Tx(sender='bob') 18 | self.run(tx, self.contract) 19 | assert self.stopped == 'Sender is not feed owner' 20 | 21 | def test_valid_sender(self): 22 | tx = Tx(sender='alice', data=['Temperature', '53.2']) 23 | self.run(tx, self.contract) 24 | assert self.contract.storage['Temperature'] == '53.2' 25 | -------------------------------------------------------------------------------- /examples/decentralized-dropbox.cll: -------------------------------------------------------------------------------- 1 | if tx.value < block.basefee * 400: 2 | stop 3 | merkle_branch = block.parenthash 4 | h = tx.data[0] 5 | i = 1 6 | while i < 26: 7 | if merkle_branch % 2 == 0: 8 | h = sha3(h + tx.data[i]) 9 | else: 10 | h = sha3(tx.data[i] + h) 11 | merkle_branch = merkle_branch / 2 12 | i += 1 13 | if h == [INSERT MERKLE TREE ROOT HASH HERE]: 14 | if contract.storage[1] < block.number: 15 | contract.storage[1] = block.number + 100 16 | send(tx.sender,10^15,0) 17 | -------------------------------------------------------------------------------- /examples/egalitarian-dao.cll: -------------------------------------------------------------------------------- 1 | if tx.value < tx.basefee * 200: 2 | stop 3 | if contract.storage[tx.sender] == 0: 4 | stop 5 | k = sha3(32,tx.data[1]) 6 | if tx.data[0] == 0: 7 | if contract.storage[k + tx.sender] == 0: 8 | contract.storage[k + tx.sender] = 1 9 | contract.storage[k] += 1 10 | else if tx.data[0] == 1: 11 | if tx.value <= tx.datan * block.basefee * 200 or contract.storage[k]: 12 | stop 13 | i = 2 14 | while i < tx.datan: 15 | contract.storage[k + i] = tx.data[i] 16 | i = i + 1 17 | contract.storage[k] = 1 18 | contract.storage[k+1] = tx.datan 19 | else if tx.data[0] == 2: 20 | if contract.storage[k] >= contract.storage[2 ^ 255 + 1] * 2 / 3: 21 | L = contract.storage[k+1] 22 | if tx.value <= L * block.basefee * 200: 23 | stop 24 | i = 3 25 | loc = contract.storage[k+2] 26 | while i < L: 27 | contract.storage[loc+i-3] = contract.storage[k+i] 28 | i = i + 1 29 | if contract.storage[2 ^ 255 + 1] == 0: 30 | contract.storage[2 ^ 255 + 1] = 1 31 | contract.storage[C] = 1 32 | -------------------------------------------------------------------------------- /examples/escrow.cll: -------------------------------------------------------------------------------- 1 | if tx.value < 100 * block.basefee: 2 | stop 3 | 4 | state = contract.storage[1000] 5 | if state == 0 and tx.value >= PRICE: 6 | contract.storage[1000] = 1 7 | contract.storage[1001] = tx.sender 8 | contract.storage[1002] = tx.value 9 | contract.storage[1003] = block.timestamp 10 | else if state == 1: 11 | if tx.sender == VERIFIER: 12 | mktx(MERCHANT, contract.storage[1002], 0, 0) 13 | contract.storage[1000] = 2 14 | else if block.timestamp > 30 * 86400 + contract.storage[1003]: 15 | mktx(contract.storage[1001], contract.storage[1002], 0, 0) 16 | contract.storage[1000] = 3 17 | -------------------------------------------------------------------------------- /examples/escrow.py: -------------------------------------------------------------------------------- 1 | from sim import Block, Contract, Simulation, Tx, mktx, stop 2 | 3 | # Constants to modify before contract creation 4 | MERCHANT = "mike" 5 | SHIPPER = "sam" 6 | PRICE_ETHER = 3995 7 | CONFIRMATION_TIMEOUT = 30 * 86400 8 | 9 | # Status enumeration 10 | S_START = 0 11 | S_CUSTOMER_PAID = 1 12 | S_SHIPPED = 2 13 | S_REFUNDED = 3 14 | 15 | # Constract Storage indexes 16 | I_STATUS = 1000 17 | I_CUSTOMER_ADDRESS = 1001 18 | I_CUSTOMER_PAID_AMOUNT = 1002 19 | I_CUSTOMER_PAID_TS = 1003 20 | 21 | MIN_FEE = 1000 22 | 23 | class Escrow(Contract): 24 | """ 25 | Escrow example to demonstrate contract basics. 26 | 27 | Customer pays specified amount. Shipper confirms shipping and releases 28 | funds to Merchant. Otherwise when not confirmed within a certain timeframe the 29 | Customer is refunded. 30 | 31 | Constants: 32 | MERCHANT - Address of the Merchant 33 | SHIPPER - Address of the Shipper 34 | PRICE_ETHER - Price of the order in Ether 35 | CONFIRMATION_TIMEOUT - Amount of seconds after which the customer can be refunded 36 | 37 | Storage: 38 | 1000: Status (I_STATUS) 39 | 0 = start (S_START) 40 | 1 = customer paid (S_CUSTOMER_PAID) 41 | 2 = shipped (S_SHIPPED) 42 | 3 = customer refunded (S_REFUNDED) 43 | 1001: Customer address (I_CUSTOMER_ADDRESS) 44 | 1002: Customer paid amount (I_CUSTOMER_PAID_AMOUNT) 45 | 1003: Customer paid timestamp (I_CUSTOMER_PAID_TS) 46 | 47 | Tx Triggers: 48 | Customer: 49 | sufficient amount => pay 50 | Shipper: 51 | mark as shipped 52 | Anyone: 53 | trigger expiration 54 | """ 55 | 56 | def run(self, tx, contract, block): 57 | if tx.value < MIN_FEE * block.basefee: 58 | stop("Insufficient fee") 59 | 60 | state = contract.storage[I_STATUS] 61 | if state == S_START and tx.value >= PRICE_ETHER: 62 | contract.storage[I_STATUS] = S_CUSTOMER_PAID 63 | contract.storage[I_CUSTOMER_ADDRESS] = tx.sender 64 | contract.storage[I_CUSTOMER_PAID_AMOUNT] = tx.value 65 | contract.storage[I_CUSTOMER_PAID_TS] = block.timestamp 66 | elif state == S_CUSTOMER_PAID: 67 | if tx.sender == SHIPPER: 68 | contract.storage[I_STATUS] = S_SHIPPED 69 | mktx(MERCHANT, contract.storage[I_CUSTOMER_PAID_AMOUNT], 0, 0) 70 | elif block.timestamp >= contract.storage[I_CUSTOMER_PAID_TS] + CONFIRMATION_TIMEOUT: 71 | contract.storage[I_STATUS] = S_REFUNDED 72 | mktx(contract.storage[I_CUSTOMER_ADDRESS], contract.storage[I_CUSTOMER_PAID_AMOUNT], 0, 0) 73 | else: 74 | stop("Invalid state transition") 75 | 76 | 77 | # Constants for test purposes 78 | CUSTOMER = "carol" 79 | TS = 1392000000 80 | 81 | class EscrowRun(Simulation): 82 | 83 | def test_insufficient_fee(self): 84 | contract = Escrow() 85 | 86 | tx = Tx(sender=CUSTOMER, value=10) 87 | self.run(tx, contract) 88 | 89 | assert self.stopped == 'Insufficient fee' 90 | 91 | def test_customer_paid(self): 92 | contract = Escrow() 93 | 94 | tx = Tx(sender=CUSTOMER, value=PRICE_ETHER) 95 | block = Block(timestamp=TS) 96 | self.run(tx, contract, block) 97 | 98 | assert contract.storage[I_STATUS] == S_CUSTOMER_PAID 99 | assert contract.storage[I_CUSTOMER_ADDRESS] == CUSTOMER 100 | assert contract.storage[I_CUSTOMER_PAID_AMOUNT] == PRICE_ETHER 101 | assert contract.storage[I_CUSTOMER_PAID_TS] == TS 102 | 103 | def test_shipped(self): 104 | contract = Escrow() 105 | 106 | tx = Tx(sender=CUSTOMER, value=PRICE_ETHER) 107 | block = Block(timestamp=TS) 108 | self.run(tx, contract, block) 109 | 110 | tx = Tx(sender=SHIPPER, value=MIN_FEE) 111 | block = Block(timestamp=TS + 1) 112 | self.run(tx, contract, block) 113 | 114 | assert contract.storage[I_STATUS] == S_SHIPPED 115 | assert len(contract.txs) == 1 116 | assert contract.txs == [(MERCHANT, PRICE_ETHER, 0, 0)] 117 | 118 | def test_confirmation_timeout(self): 119 | contract = Escrow() 120 | 121 | tx = Tx(sender=CUSTOMER, value=PRICE_ETHER) 122 | block = Block(timestamp=TS) 123 | self.run(tx, contract, block) 124 | 125 | tx = Tx(sender=CUSTOMER, value=MIN_FEE) 126 | block = Block(timestamp=TS + CONFIRMATION_TIMEOUT + 1) 127 | self.run(tx, contract, block) 128 | 129 | assert contract.storage[I_STATUS] == S_REFUNDED 130 | assert len(contract.txs) == 1 131 | assert contract.txs == [(CUSTOMER, PRICE_ETHER, 0, 0)] 132 | -------------------------------------------------------------------------------- /examples/fountain.cll: -------------------------------------------------------------------------------- 1 | if tx.value < 1000 * block.basefee: 2 | stop 3 | to = tx.data[0] 4 | value = tx.value - 1000 * block.basefee 5 | if block.address_balance(to) == 0: 6 | mktx(to, value, 0, 0) 7 | else: 8 | mktx(tx.sender, value, 0, 0) 9 | -------------------------------------------------------------------------------- /examples/fountain.py: -------------------------------------------------------------------------------- 1 | from sim import Block, Contract, Simulation, Tx, mktx, stop 2 | 3 | class Fountain(Contract): 4 | """Fountain example to demonstrate block.account_balance""" 5 | 6 | def run(self, tx, contract, block): 7 | if tx.value < 1000 * block.basefee: 8 | stop("Insufficient fee") 9 | to = tx.data[0] 10 | value = tx.value - 1000 * block.basefee 11 | if block.account_balance(to) == 0: 12 | mktx(to, value, 0, 0) 13 | else: 14 | mktx(tx.sender, value, 0, 0) 15 | 16 | 17 | class FountainRun(Simulation): 18 | 19 | contract = Fountain() 20 | 21 | def test_insufficient_fee(self): 22 | tx = Tx(sender='alice', value=10) 23 | self.run(tx, self.contract) 24 | assert self.stopped == 'Insufficient fee' 25 | 26 | def test_recipient_has_no_balance(self): 27 | tx = Tx(sender='alice', value=2000, data=['bob']) 28 | block = Block() 29 | self.run(tx, self.contract, block) 30 | assert len(self.contract.txs) == 1 31 | assert self.contract.txs == [('bob', 1000, 0, 0)] 32 | 33 | def test_recipient_has_balance(self): 34 | tx = Tx(sender='alice', value=2000, data=['bob']) 35 | block = Block() 36 | block.set_account_balance('bob', 1000) 37 | self.run(tx, self.contract, block) 38 | assert len(self.contract.txs) == 1 39 | assert self.contract.txs == [('alice', 1000, 0, 0)] 40 | -------------------------------------------------------------------------------- /examples/hedging.cll: -------------------------------------------------------------------------------- 1 | if tx.value < 200 * block.basefee: 2 | stop 3 | if contract.storage[1000] == 0: 4 | if tx.value < 1000 * 10^18: 5 | stop 6 | contract.storage[1000] = 1 7 | contract.storage[1001] = 998 * block.contract_storage(D)[I] 8 | contract.storage[1002] = block.timestamp + 30 * 86400 9 | contract.storage[1003] = tx.sender 10 | else: 11 | ethervalue = contract.storage[1001] / block.contract_storage(D)[I] 12 | if ethervalue >= 5000: 13 | mktx(contract.storage[1003],5000 * 10^18,0,0) 14 | else if block.timestamp > contract.storage[1002]: 15 | mktx(contract.storage[1003],ethervalue * 10^18,0,0) 16 | mktx(A,(5000 - ethervalue) * 10^18,0,0) 17 | -------------------------------------------------------------------------------- /examples/hedging.py: -------------------------------------------------------------------------------- 1 | from sim import Block, Contract, Simulation, Tx, log, mktx, stop 2 | 3 | class FinancialDerivative(Contract): 4 | """Financial derivatives contract example from https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-White-Paper#wiki-financial-derivatives""" 5 | 6 | def run(self, tx, contract, block): 7 | if tx.value < 200 * block.basefee: 8 | stop("Insufficient fee") 9 | if contract.storage[1000] == 0: 10 | if tx.value < 1000 * 10 ** 18: 11 | stop("Insufficient value") 12 | contract.storage[1000] = 1 # XXX Bug in contract example flag should be set 13 | contract.storage[1001] = 998 * block.contract_storage(D)[I] 14 | contract.storage[1002] = block.timestamp + 30 * 86400 15 | contract.storage[1003] = tx.sender 16 | log("Contract initialized") 17 | else: 18 | ethervalue = contract.storage[1001] / block.contract_storage(D)[I] 19 | log("Ether Value = %s" % ethervalue) 20 | if ethervalue >= 5000: # XXX Bug in contract example, value shouldn't be times 10 ** 18 21 | mktx(contract.storage[1003], 5000 * 10 ** 18, 0, 0) 22 | elif block.timestamp > contract.storage[1002]: 23 | # XXX Bug in contract example, values should be times 10 ** 18 24 | mktx(contract.storage[1003], ethervalue * 10 ** 18, 0, 0) 25 | mktx(A, (5000 - ethervalue) * 10 ** 18, 0, 0) 26 | 27 | 28 | class HedgingRun(Simulation): 29 | 30 | contract = FinancialDerivative(A="alice", D="datafeed", I="USD") 31 | ts_zero = 1392632520 32 | 33 | def test_insufficient_fee(self): 34 | tx = Tx(sender='alice', value=10) 35 | self.run(tx, self.contract) 36 | assert self.stopped == 'Insufficient fee' 37 | 38 | def test_insufficient_value(self): 39 | tx = Tx(sender='alice', value=1000) 40 | self.run(tx, self.contract) 41 | assert self.stopped == 'Insufficient value' 42 | assert self.contract.storage[1000] == 0 43 | 44 | def test_creation(self): 45 | block = Block(timestamp=self.ts_zero) 46 | block.contract_storage(self.contract.D)[self.contract.I] = 2500 47 | tx = Tx(sender='bob', value=1000 * 10 ** 18) 48 | self.run(tx, self.contract, block) 49 | assert self.contract.storage[1000] == 1 50 | assert self.contract.storage[1001] == 2495000 51 | assert self.contract.storage[1002] == self.ts_zero + 30 * 86400 52 | assert self.contract.storage[1003] == tx.sender 53 | assert len(self.contract.txs) == 0 54 | 55 | def test_ether_drops(self): 56 | block = Block(timestamp=self.ts_zero + 30 * 86400 + 1) 57 | block.contract_storage(self.contract.D)[self.contract.I] = 400 58 | tx = Tx(sender='bob', value=200) 59 | self.run(tx, self.contract, block) 60 | assert len(self.contract.txs) == 1 61 | assert self.contract.txs == [('bob', 5000 * 10 ** 18, 0, 0)] 62 | 63 | def test_ether_rises(self): 64 | block = Block(timestamp=self.ts_zero + 30 * 86400 + 1) 65 | block.contract_storage(self.contract.D)[self.contract.I] = 4000 66 | tx = Tx(sender='bob', value=200) 67 | self.run(tx, self.contract, block) 68 | assert len(self.contract.txs) == 2 69 | assert self.contract.txs == [('bob', 623 * 10 ** 18, 0, 0), ('alice', 4377 * 10 ** 18, 0, 0)] 70 | -------------------------------------------------------------------------------- /examples/i_want_half.cll: -------------------------------------------------------------------------------- 1 | # Eddie Murphy - I want half 2 | # https://www.youtube.com/watch?v=Q4YJHvzo2io 3 | # by: 4 | # - Yves Candel 5 | # - Nick Savers 6 | # - Joris Bontje 7 | 8 | # Constract Storage indexes 9 | I_STATE = 1000 10 | I_PARTNER_1 = 1001 11 | I_PARTNER_2 = 1002 12 | I_WITHDRAW_TO = 1003 13 | I_WITHDRAW_AMOUNT = 1004 14 | I_WITHDRAW_CREATOR = 1005 15 | I_DIVORCE_CREATOR = 1006 16 | 17 | C_CANCEL_PERIOD = 1000 18 | 19 | # Status enumeration 20 | S_START = 0 21 | S_PROPOSED = 1 22 | S_MARRIED = 2 23 | S_DIVORCED = 3 24 | 25 | # Transaction triggers 26 | TX_WITHDRAW = 1 27 | TX_DIVORCE = 2 28 | 29 | if tx.value < 100 * block.basefee: 30 | stop 31 | 32 | state = contract.storage[I_STATE] 33 | if state == S_START: 34 | contract.storage[I_PARTNER_1] = tx.sender 35 | contract.storage[I_PARTNER_2] = tx.data[0] 36 | contract.storage[I_STATE] = S_PROPOSED 37 | stop 38 | 39 | partner_1 = contract.storage[I_PARTNER_1] 40 | partner_2 = contract.storage[I_PARTNER_2] 41 | 42 | if state == S_PROPOSED: 43 | if tx.sender == partner_2 and tx.data[0] == partner_1: 44 | contract.storage[I_STATE] = S_MARRIED 45 | else if tx.sender == partner_1 and tx.data[0] == partner_1: 46 | if block.timestamp < C_CANCEL_PERIOD: 47 | stop 48 | contract.storage[I_STATE] = S_START 49 | mktx(tx.sender, block.account_balance(contract.address), 0, 0) 50 | 51 | else if state == S_MARRIED and tx.sender == partner_1 or tx.sender == partner_2: 52 | if tx.data[0] == TX_WITHDRAW: 53 | creator = contract.storage[I_WITHDRAW_CREATOR] 54 | if creator != 0 and contract.storage[I_WITHDRAW_TO] == tx.data[1] and contract.storage[I_WITHDRAW_AMOUNT] == tx.data[2] and creator != tx.sender: 55 | mktx(tx.data[1], tx.data[2], 0, 0) 56 | contract.storage[I_WITHDRAW_TO] = 0 57 | contract.storage[I_WITHDRAW_AMOUNT] = 0 58 | contract.storage[I_WITHDRAW_CREATOR] = 0 59 | else: 60 | contract.storage[I_WITHDRAW_TO] = tx.data[1] 61 | contract.storage[I_WITHDRAW_AMOUNT] = tx.data[2] 62 | contract.storage[I_WITHDRAW_CREATOR] = tx.sender 63 | else if tx.data[0] == TX_DIVORCE: 64 | creator = contract.storage[I_DIVORCE_CREATOR] 65 | if creator != 0 and creator != tx.sender: 66 | balance = block.account_balance(contract.address) 67 | mktx(partner_1, balance / 2, 0, 0) 68 | mktx(partner_2, balance / 2, 0, 0) 69 | contract.storage[I_STATE] = S_DIVORCED 70 | else: 71 | contract.storage[I_DIVORCE_CREATOR] = tx.sender 72 | -------------------------------------------------------------------------------- /examples/i_want_half.py: -------------------------------------------------------------------------------- 1 | from sim import Block, Contract, Simulation, Tx, mktx, stop 2 | 3 | # Marriage contract with divorce clause. 4 | # Inspired by Eddie Murphy - I want half 5 | # https://www.youtube.com/watch?v=Q4YJHvzo2io 6 | # 7 | # by: 8 | # - Yves Candel 9 | # - Nick Savers 10 | # - Joris Bontje 11 | 12 | # Constract Storage indexes 13 | I_STATE = 1000 14 | I_PARTNER_1 = 1001 15 | I_PARTNER_2 = 1002 16 | I_WITHDRAW_TO = 1003 17 | I_WITHDRAW_AMOUNT = 1004 18 | I_WITHDRAW_CREATOR = 1005 19 | I_DIVORCE_CREATOR = 1006 20 | 21 | C_CANCEL_PERIOD = 1000 22 | 23 | # Status enumeration 24 | S_START = 0 25 | S_PROPOSED = 1 26 | S_MARRIED = 2 27 | S_DIVORCED = 3 28 | 29 | # Transaction triggers 30 | TX_WITHDRAW = 1 31 | TX_DIVORCE = 2 32 | 33 | class Marriage(Contract): 34 | """ 35 | Marriage contract with divorce clause. 36 | Inspired by Eddie Murphy - I want half 37 | https://www.youtube.com/watch?v=Q4YJHvzo2io 38 | """ 39 | def run(self, tx, contract, block): 40 | if tx.value < 100 * block.basefee: 41 | stop("Insufficient fee") 42 | 43 | state = contract.storage[I_STATE] 44 | if state == S_START: 45 | contract.storage[I_PARTNER_1] = tx.sender 46 | contract.storage[I_PARTNER_2] = tx.data[0] 47 | contract.storage[I_STATE] = S_PROPOSED 48 | stop("Proposed") 49 | partner_1 = contract.storage[I_PARTNER_1] 50 | partner_2 = contract.storage[I_PARTNER_2] 51 | 52 | if state == S_PROPOSED: 53 | if tx.sender == partner_2 and tx.data[0] == partner_1: 54 | contract.storage[I_STATE] = S_MARRIED 55 | stop("Married") 56 | if tx.sender == partner_1 and tx.data[0] == partner_1: 57 | if block.timestamp < C_CANCEL_PERIOD: 58 | stop("Cant cancel early") 59 | contract.storage[I_STATE] = S_START 60 | mktx(tx.sender, block.account_balance(contract.address), 0, 0) 61 | stop("Cancelled") 62 | stop("Invalid during proposal") 63 | if state == S_MARRIED and tx.sender == partner_1 or tx.sender == partner_2: 64 | if tx.data[0] == TX_WITHDRAW: 65 | creator = contract.storage[I_WITHDRAW_CREATOR] 66 | if (creator != 0 and contract.storage[I_WITHDRAW_TO] == tx.data[1] and 67 | contract.storage[I_WITHDRAW_AMOUNT] == tx.data[2] and creator != tx.sender): 68 | mktx(tx.data[1], tx.data[2], 0, 0) 69 | contract.storage[I_WITHDRAW_TO] = 0 70 | contract.storage[I_WITHDRAW_AMOUNT] = 0 71 | contract.storage[I_WITHDRAW_CREATOR] = 0 72 | stop("Withdrawed") 73 | else: 74 | contract.storage[I_WITHDRAW_TO] = tx.data[1] 75 | contract.storage[I_WITHDRAW_AMOUNT] = tx.data[2] 76 | contract.storage[I_WITHDRAW_CREATOR] = tx.sender 77 | stop("Withdraw requested") 78 | if tx.data[0] == TX_DIVORCE: 79 | creator = contract.storage[I_DIVORCE_CREATOR] 80 | if creator != 0 and creator != tx.sender: 81 | balance = block.account_balance(contract.address) 82 | mktx(partner_1, balance / 2, 0, 0) 83 | mktx(partner_2, balance / 2, 0, 0) 84 | contract.storage[I_STATE] = S_DIVORCED 85 | stop("Divorced") 86 | else: 87 | contract.storage[I_DIVORCE_CREATOR] = tx.sender 88 | stop("Divorce requested") 89 | stop("Invalid during marriage") 90 | stop("Should be divorced") 91 | 92 | PARTNER_1 = "alice" 93 | PARTNER_2 = "eddie" 94 | MERCHANT_ADDRESS = "Butterfly Labs" 95 | MERCHANT_AMOUNT = 99999 96 | 97 | class MarriageRun(Simulation): 98 | 99 | contract = Marriage() 100 | 101 | def test_insufficient_fee(self): 102 | 103 | tx = Tx(sender=PARTNER_1, value=10) 104 | self.run(tx, self.contract) 105 | 106 | assert self.stopped == "Insufficient fee" 107 | 108 | def test_proposal(self): 109 | tx = Tx(sender=PARTNER_1, value=100, data=[PARTNER_2]) 110 | self.run(tx, self.contract) 111 | 112 | assert self.contract.storage[I_PARTNER_1] == PARTNER_1 113 | assert self.contract.storage[I_PARTNER_2] == PARTNER_2 114 | assert self.contract.storage[I_STATE] == S_PROPOSED 115 | assert self.stopped == "Proposed" 116 | 117 | def test_cancel_proposal(self, early=0): 118 | tx = Tx(sender=PARTNER_1, value=100, data=[PARTNER_1]) 119 | self.run(tx, self.contract, Block(timestamp=(0 if early else 2000))) 120 | if early: 121 | assert self.stopped == "Cant cancel early" 122 | else: 123 | assert self.stopped == "Cancelled" 124 | 125 | def test_failed_cancelled_proposal(self): 126 | self.test_proposal() # Re-propose, as the above should cancel it. 127 | self.test_cancel_proposal(early=1) 128 | 129 | def test_withdraw_not_married_fails(self): 130 | tx = Tx(sender=PARTNER_1, value=100, data=[TX_WITHDRAW, MERCHANT_ADDRESS, MERCHANT_AMOUNT]) 131 | self.run(tx, self.contract) 132 | 133 | assert self.contract.storage[I_WITHDRAW_TO] == 0 134 | assert self.contract.storage[I_WITHDRAW_AMOUNT] == 0 135 | assert self.contract.storage[I_WITHDRAW_CREATOR] == 0 136 | assert self.stopped == "Invalid during proposal" 137 | 138 | def test_accept(self): 139 | tx = Tx(sender=PARTNER_2, value=100, data=[PARTNER_1]) 140 | self.run(tx, self.contract) 141 | 142 | assert self.contract.storage[I_PARTNER_1] == PARTNER_1 143 | assert self.contract.storage[I_PARTNER_2] == PARTNER_2 144 | assert self.contract.storage[I_STATE] == S_MARRIED 145 | assert self.stopped == "Married" 146 | 147 | def test_withdraw_request(self): 148 | tx = Tx(sender=PARTNER_1, value=100, data=[TX_WITHDRAW, MERCHANT_ADDRESS, MERCHANT_AMOUNT]) 149 | self.run(tx, self.contract) 150 | 151 | assert self.contract.storage[I_WITHDRAW_TO] == MERCHANT_ADDRESS 152 | assert self.contract.storage[I_WITHDRAW_AMOUNT] == MERCHANT_AMOUNT 153 | assert self.contract.storage[I_WITHDRAW_CREATOR] == PARTNER_1 154 | assert self.stopped == "Withdraw requested" 155 | 156 | def test_withdraw_approval(self): 157 | tx = Tx(sender=PARTNER_2, value=100, data=[TX_WITHDRAW, MERCHANT_ADDRESS, MERCHANT_AMOUNT]) 158 | self.run(tx, self.contract) 159 | 160 | assert len(self.contract.txs) == 1 161 | assert self.contract.txs == [(MERCHANT_ADDRESS, MERCHANT_AMOUNT, 0, 0)] 162 | assert self.stopped == "Withdrawed" 163 | 164 | def test_divorce_request(self): 165 | tx = Tx(sender=PARTNER_1, value=100, data=[TX_DIVORCE]) 166 | self.run(tx, self.contract) 167 | 168 | assert self.contract.storage[I_DIVORCE_CREATOR] == PARTNER_1 169 | assert self.stopped == "Divorce requested" 170 | 171 | def test_divorce_approval(self): 172 | tx = Tx(sender=PARTNER_2, value=100, data=[TX_DIVORCE]) 173 | 174 | block = Block() 175 | block.set_account_balance('myaddress', 1000) 176 | self.run(tx, self.contract, block) 177 | 178 | assert self.contract.storage[I_STATE] == S_DIVORCED 179 | assert len(self.contract.txs) == 2 180 | assert self.contract.txs == [(PARTNER_1, 500, 0, 0), (PARTNER_2, 500, 0, 0)] 181 | assert self.stopped == "Divorced" 182 | 183 | def test_withdraw_after_divorce_fails(self): 184 | tx = Tx(sender=PARTNER_1, value=100, data=[TX_WITHDRAW, MERCHANT_ADDRESS, MERCHANT_AMOUNT]) 185 | self.run(tx, self.contract) 186 | 187 | assert self.contract.storage[I_WITHDRAW_TO] == 0 188 | assert self.contract.storage[I_WITHDRAW_AMOUNT] == 0 189 | assert self.contract.storage[I_WITHDRAW_CREATOR] == 0 190 | assert self.stopped == "Should be divorced" 191 | -------------------------------------------------------------------------------- /examples/lockin-escrow.cll: -------------------------------------------------------------------------------- 1 | if tx.value < MIN_FEE * block.basefee: 2 | stop # Insufficient fee 3 | customer = contract.storage[I_CUSTOMER] 4 | if tx.sender == MERCHANT: 5 | if block.account_balance(contract.address) < MIN_BALANCE: 6 | stop # Below funds of operation 7 | if tx.data[0] == C_ALLOW: 8 | if customer != 0: # ..when earlier customer still busy. 9 | stop # Customer change blocked 10 | contract.storage[I_CUSTOMER] = tx.data[1] 11 | contract.storage[I_TOTAL] = tx.data[2] 12 | contract.storage[I_INCENTIVE] = tx.data[3] 13 | stop # Customer allowed 14 | if tx.data[0] == C_REFUND and customer != 0: 15 | refund = contract.storage[I_PAID] 16 | refund += contract.storage[I_PAID]*MIN_BALANCE/contract.storage[I_TOTAL] 17 | mktx(customer, min(refund, contract.storage[I_TOTAL]+MIN_BALANCE), 0, []) 18 | contract.storage[I_CUSTOMER] = 0 19 | contract.storage[I_TOTAL] = 0 20 | contract.storage[I_INCENTIVE] = 0 21 | contract.storage[I_PAID] = 0 22 | stop # Customer refunded 23 | stop # Merchant topping up 24 | if tx.sender == customer: 25 | contract.storage[I_PAID] = contract.storage[I_PAID] + tx.value - MIN_FEE*block.basefee 26 | if tx.datan == 1 and tx.data[0] == C_SATISFIED: 27 | if contract.storage[I_PAID] <= contract.storage[I_TOTAL]: 28 | stop # Customer didnt pay enough 29 | mktx(MERCHANT, contract.storage[I_PAID], 0, []) 30 | mktx(customer, contract.storage[I_INCENTIVE], 0, []) 31 | contract.storage[I_CUSTOMER] = 0 32 | contract.storage[I_TOTAL] = 0 33 | contract.storage[I_INCENTIVE] = 0 34 | contract.storage[I_PAID] = 0 35 | stop # Customer paid and happy 36 | stop # Customer paid(part) 37 | stop # Donation 38 | -------------------------------------------------------------------------------- /examples/lockin-escrow.py: -------------------------------------------------------------------------------- 1 | from sim import Block, Contract, Simulation, Tx, mktx, stop 2 | from random import random 3 | import inspect 4 | 5 | # Constants to modify before contract creation. 6 | CUSTOMER = "carol" 7 | MERCHANT = "mike" 8 | 9 | MIN_FEE = 1000 10 | MIN_BALANCE = 3000 # Merchants stake. 11 | 12 | PRICE = 39950 13 | INCENTIVE = 10000 14 | TOTAL = PRICE+INCENTIVE 15 | 16 | # Merchant can: 17 | C_ALLOW = 1 # Allows a customer to use the script. 18 | C_REFUND = 2 # Refund initiated by merchant. 19 | 20 | # Customer can: 21 | C_SATISFIED = 1 # Indicate satisfaction. 22 | # (failing to do so will lock up the funds) 23 | 24 | # Constract Storage indexes 25 | I_CUSTOMER = "customer" # 1000 26 | I_INCENTIVE = "incentive" # 1001 27 | I_TOTAL = "total" # 1002 28 | I_PAID = "paid" # 1003 29 | 30 | 31 | class LockinEscrow(Contract): 32 | """ 33 | Escrow without third party. Really just insures it is in no-ones interest 34 | to fail. 35 | 36 | If the customer doesnt indicate he is happy, the only way to make the 37 | contract usable for the merchant is to refund more than was ever paid into. 38 | 39 | Counterexample of the idea that ethereum contracts cant do more than the 40 | real world can, even though it is clearly less desirable than 'real' 41 | escrow. 42 | 43 | NOTE: 'Offered refunds' might be nice; those would have to be accepted by 44 | the customer, but dont involve any MIN_BALANCE being taken away from the 45 | merchant. 46 | """ 47 | 48 | def run(self, tx, contract, block): 49 | if tx.value < MIN_FEE * block.basefee: 50 | stop("Insufficient fee") 51 | 52 | customer = contract.storage[I_CUSTOMER] 53 | 54 | if tx.sender == MERCHANT: 55 | if block.account_balance(contract.address) < MIN_BALANCE: 56 | stop("Below funds of operation") 57 | if tx.data[0] == C_ALLOW: 58 | if customer != 0: # ..when earlier customer still busy. 59 | stop("Customer change blocked") 60 | contract.storage[I_CUSTOMER] = tx.data[1] 61 | contract.storage[I_TOTAL] = tx.data[2] 62 | contract.storage[I_INCENTIVE] = tx.data[3] 63 | stop("Customer allowed") 64 | if tx.data[0] == C_REFUND and customer != 0: 65 | refund = contract.storage[I_PAID] 66 | refund += contract.storage[I_PAID]*MIN_BALANCE/contract.storage[I_TOTAL] 67 | 68 | mktx(customer, min(refund, contract.storage[I_TOTAL]+MIN_BALANCE), 0, []) 69 | contract.storage[I_CUSTOMER] = 0 70 | contract.storage[I_TOTAL] = 0 71 | contract.storage[I_INCENTIVE] = 0 72 | contract.storage[I_PAID] = 0 73 | stop("Customer refunded") 74 | stop("Merchant topping up") 75 | 76 | if tx.sender == customer: 77 | contract.storage[I_PAID] = contract.storage[I_PAID] + tx.value - MIN_FEE*block.basefee 78 | if tx.datan == 1 and tx.data[0] == C_SATISFIED: 79 | if contract.storage[I_PAID] <= contract.storage[I_TOTAL]: 80 | stop("Customer didnt pay enough") 81 | incentive = contract.storage[I_INCENTIVE] 82 | mktx(MERCHANT, contract.storage[I_PAID]-incentive, 0, []) 83 | mktx(customer, incentive, 0, []) 84 | contract.storage[I_CUSTOMER] = 0 85 | contract.storage[I_TOTAL] = 0 86 | contract.storage[I_INCENTIVE] = 0 87 | contract.storage[I_PAID] = 0 88 | stop("Customer paid and happy") 89 | stop("Customer paid(part)") 90 | stop("Donation") 91 | 92 | def random_incentive(): 93 | return INCENTIVE*(0.1 + random()) 94 | 95 | class LockinEscrowRun(Simulation): 96 | 97 | contract = LockinEscrow() 98 | block = Block() 99 | 100 | total = 0 101 | incentive = 0 102 | paid = 0 103 | 104 | def reset(self): 105 | self.contract = LockinEscrow() 106 | self.total = 0 107 | self.incentive = 0 108 | self.paid = 0 109 | 110 | def run_tx(self, value=0, sender="", data=[]): 111 | self.run(Tx(value=value, sender=sender, data=data), self.contract, self.block, 112 | method_name=inspect.stack()[1][3]) 113 | 114 | def test_donate(self, value=max(MIN_FEE, random()*TOTAL)): 115 | self.run_tx(sender="anyone", value=value) 116 | assert self.stopped == "Donation" 117 | 118 | def test_merchant_under_balance(self): 119 | self.contact = LockinEscrow() 120 | self.run_tx(sender=MERCHANT, value=random()*MIN_BALANCE*0.9) 121 | self.stopped == "Below funds of operation" 122 | 123 | def test_merchant_allow(self): 124 | #Test intended when contract not busy. 125 | assert self.contract.storage[I_CUSTOMER] == 0 126 | 127 | self.block.set_account_balance(self.contract.address, MIN_BALANCE) 128 | self.incentive = random_incentive() 129 | self.total = PRICE + self.incentive 130 | self.run_tx(sender=MERCHANT, value=MIN_BALANCE + MIN_FEE, 131 | data=[C_ALLOW, CUSTOMER, self.total, self.incentive]) 132 | assert self.stopped == "Customer allowed" 133 | assert self.contract.storage[I_CUSTOMER] == CUSTOMER 134 | assert self.contract.storage[I_TOTAL] == self.total 135 | assert self.contract.storage[I_INCENTIVE] == self.incentive 136 | assert self.contract.storage[I_PAID] == 0 137 | 138 | def test_customer_change_blocked(self): 139 | r_incentive = random_incentive() 140 | self.run_tx(sender=MERCHANT, value=MIN_BALANCE + MIN_FEE, 141 | data=[C_ALLOW, CUSTOMER, 2*TOTAL, r_incentive]) 142 | assert self.stopped == "Customer change blocked" 143 | assert self.contract.storage[I_TOTAL] == self.total 144 | assert self.contract.storage[I_INCENTIVE] == self.incentive 145 | 146 | def test_customer_pay(self): 147 | self.paid = random()*PRICE 148 | self.run_tx(sender=CUSTOMER, value=self.paid + MIN_FEE) 149 | assert self.stopped == "Customer paid(part)" 150 | 151 | def test_customer_pay_too_little(self): 152 | self.reset() 153 | self.test_merchant_allow() 154 | self.paid = random()*0.9*self.total 155 | self.run_tx(sender=CUSTOMER, value=self.paid + MIN_FEE, data=[C_SATISFIED]) 156 | assert self.stopped == "Customer didnt pay enough" 157 | assert len(self.contract.txs) == 0 158 | 159 | def assert_reset(self): 160 | assert self.contract.storage[I_CUSTOMER] == 0 161 | assert self.contract.storage[I_TOTAL] == 0 162 | assert self.contract.storage[I_INCENTIVE] == 0 163 | assert self.contract.storage[I_PAID] == 0 164 | 165 | def assert_happy(self): 166 | assert self.stopped == "Customer paid and happy" 167 | self.assert_reset() 168 | 169 | def test_customer_pay_and_happy(self): 170 | self.reset() 171 | self.test_merchant_allow() 172 | self.paid = self.total + 1 173 | self.run_tx(sender=CUSTOMER, value=self.paid + MIN_FEE, data=[C_SATISFIED]) 174 | assert self.contract.txs[0][0] == MERCHANT 175 | assert self.contract.txs[0][1] == self.paid - self.incentive 176 | assert self.contract.txs[1][0] == CUSTOMER 177 | assert self.contract.txs[1][1] == self.incentive 178 | self.assert_happy() 179 | 180 | def test_customer_pay_part(self): 181 | self.assert_reset() 182 | self.test_merchant_allow() 183 | self.paid = self.total + 1 184 | self.run_tx(sender=CUSTOMER, value=self.paid + MIN_FEE) 185 | assert self.stopped == "Customer paid(part)" # (all, actually) 186 | assert self.contract.storage[I_PAID] == self.total + 1 187 | 188 | def test_customer_happy(self): # depends on the pay one being run first. 189 | self.paid += 1 190 | self.run_tx(sender=CUSTOMER, value=MIN_FEE + 1, data=[C_SATISFIED]) 191 | assert self.contract.txs[0][0] == MERCHANT 192 | assert self.contract.txs[0][1] == self.paid - self.incentive 193 | assert self.contract.txs[1][0] == CUSTOMER 194 | assert self.contract.txs[1][1] == self.incentive 195 | self.assert_happy() 196 | 197 | def test_refund(self): 198 | self.test_customer_pay_part() 199 | self.run_tx(sender=MERCHANT, value=MIN_FEE, data=[C_REFUND]) 200 | assert self.stopped == "Customer refunded" 201 | assert self.contract.txs[0][0] == CUSTOMER 202 | assert self.contract.txs[0][1] == min(self.paid*(1 + MIN_BALANCE/self.total), 203 | self.total + MIN_BALANCE) 204 | self.assert_reset() 205 | -------------------------------------------------------------------------------- /examples/namecoin.cll: -------------------------------------------------------------------------------- 1 | if msg.sender >= 1000: 2 | contract.storage[msg.sender] = msg.data[0] -------------------------------------------------------------------------------- /examples/namecoin.py: -------------------------------------------------------------------------------- 1 | from sim import Contract, Simulation, Tx, stop 2 | 3 | class Namecoin(Contract): 4 | """Namecoin contract example from https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-White-Paper#wiki-identity-and-reputation-systems""" 5 | 6 | def run(self, tx, contract, block): 7 | if tx.value < block.basefee * 200: 8 | stop("Insufficient fee") 9 | if contract.storage[tx.data[0]] or tx.data[0] < 100: 10 | stop("Key already reserved") 11 | contract.storage[tx.data[0]] = tx.data[1] 12 | 13 | 14 | class NamecoinRun(Simulation): 15 | 16 | contract = Namecoin() 17 | 18 | def test_insufficient_fee(self): 19 | tx = Tx(sender='alice', value=10) 20 | self.run(tx, self.contract) 21 | assert self.stopped == 'Insufficient fee' 22 | 23 | def test_reservation(self): 24 | tx = Tx(sender='alice', value=200, data=['ethereum.bit', '54.200.236.204']) 25 | self.run(tx, self.contract) 26 | assert self.contract.storage['ethereum.bit'] == '54.200.236.204' 27 | 28 | def test_double_reservation(self): 29 | tx = Tx(sender='alice', value=200, data=['ethereum.bit', '127.0.0.1']) 30 | self.run(tx, self.contract) 31 | assert self.stopped == 'Key already reserved' 32 | assert self.contract.storage['ethereum.bit'] == '54.200.236.204' 33 | -------------------------------------------------------------------------------- /examples/subcurrency.cll: -------------------------------------------------------------------------------- 1 | if tx.value < 100 * block.basefee: 2 | stop // Insufficient fee 3 | elif contract.storage[1000]: // Running contract... 4 | frm = tx.sender 5 | to = tx.data[0] 6 | value = tx.data[1] 7 | if to <= 1000: 8 | stop // "tx.data[0] out of bounds: %s" % tx.data[0] 9 | bal = contract.storage[frm] // "Balance of %s: %d" % (frm, bal) 10 | if bal < value: 11 | stop // "Insufficient funds, %s has %d needs %d" % (frm, bal, value) 12 | else: // "Adjusting values by %d" % value 13 | contract.storage[frm] = contract.storage[frm] - value 14 | contract.storage[to] = contract.storage[to] + value 15 | else: 16 | contract.storage[tx.sender] = 10^18 // #define tx.sender=MYCREATOR 17 | contract.storage[1000] = 1 // "Initializing storage for creator %s" % tx.sender 18 | -------------------------------------------------------------------------------- /examples/subcurrency.py: -------------------------------------------------------------------------------- 1 | from sim import Contract, Simulation, Tx, log, stop 2 | 3 | class SubCurrency(Contract): 4 | """Sub-currency contract example from https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-White-Paper#wiki-sub-currencies""" 5 | 6 | def run(self, tx, contract, block): 7 | Contract.load(self, "examples/subcurrency.cll", tx, contract, block) 8 | 9 | 10 | class SubCurrencyRun(Simulation): 11 | 12 | contract = SubCurrency(MYCREATOR="alice") 13 | 14 | def test_insufficient_fee(self): 15 | tx = Tx(sender='alice', value=10) 16 | self.run(tx, self.contract) 17 | assert self.stopped == 'Insufficient fee' 18 | 19 | def test_creation(self): 20 | tx = Tx(sender='alice', value=100) 21 | self.run(tx, self.contract) 22 | assert self.contract.storage['alice'] == 10 ** 18 23 | 24 | def test_alice_to_bob(self): 25 | tx = Tx(sender='alice', value=100, data=['bob', 1000]) 26 | self.run(tx, self.contract) 27 | assert self.contract.storage['alice'] == 10 ** 18 - 1000 28 | assert self.contract.storage['bob'] == 1000 29 | 30 | def test_alice_to_invalid(self): 31 | tx = Tx(sender='alice', value=100, data=[123, 1000]) 32 | self.run(tx, self.contract) 33 | assert self.stopped == 'tx.data[0] out of bounds: 123' 34 | assert self.contract.storage['bob'] == 1000 35 | assert self.contract.storage['charlie'] == 0 36 | 37 | def test_bob_to_charlie_invalid(self): 38 | tx = Tx(sender='bob', value=100, data=['charlie', 1001]) 39 | self.run(tx, self.contract) 40 | assert self.stopped == 'Insufficient funds, bob has 1000 needs 1001' 41 | assert self.contract.storage['bob'] == 1000 42 | assert self.contract.storage['charlie'] == 0 43 | 44 | def test_bob_to_charlie_valid(self): 45 | tx = Tx(sender='bob', value=100, data=['charlie', 1000]) 46 | self.run(tx, self.contract) 47 | assert self.contract.storage['bob'] == 0 48 | assert self.contract.storage['charlie'] == 1000 49 | 50 | def test_storage_result(self): 51 | self.log(self.contract.storage) 52 | 53 | # # Python module export 54 | # def test_export(self): 55 | # print "\nClosure module\n===" 56 | # print self.contract.closure 57 | # print "\n===\nEnd module" 58 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/cll-sim/ccb5f824e2165fdbcb7d86be0d70af78816e83c2/lib/__init__.py -------------------------------------------------------------------------------- /lib/sim.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import os, sys, imp 3 | import inspect 4 | import logging 5 | from operator import itemgetter 6 | MINGASPRICE = 10000000000000 7 | 8 | def _modify_frame_global(key, value, stack=None, offset=2): 9 | if stack is None: 10 | stack = inspect.stack() 11 | stack[offset][0].f_globals[key] = value 12 | 13 | def _infer_self(stack=None, offset=2): 14 | if stack is None: 15 | stack = inspect.stack() 16 | return stack[offset][0].f_locals['self'] 17 | 18 | def _is_called_by_contract(): 19 | self = _infer_self(inspect.stack()) 20 | caller_class = self.__class__ 21 | return Contract in caller_class.__bases__ 22 | 23 | def mktx(recipient, amount, datan, data): 24 | self = _infer_self(inspect.stack()) 25 | logging.info("Sending tx to %s of %s" % (recipient, amount)) 26 | self.txs.append((recipient, amount, datan, data)) 27 | 28 | def send(recipient, amount, gas): 29 | self = _infer_self(inspect.stack()) 30 | logging.info("Sending tx to %s of %s" % (recipient, amount)) 31 | self.txs.append((recipient, amount, datan, data)) 32 | 33 | def mkmsg(recipient, amount, gas, data, datan): 34 | self = _infer_self(inspect.stack()) 35 | logging.info("Sending tx to %s of %s with data %s" % (recipient, amount, data)) 36 | self.txs.append((recipient, amount, datan, data)) 37 | 38 | def stop(reason): 39 | raise Stop(reason) 40 | 41 | def stopret(value, index=None): 42 | if index: 43 | raise Stop(value[index]) 44 | else: 45 | raise Stop(value) 46 | 47 | def array(n): 48 | return [None] * n 49 | 50 | def suicide(address): 51 | self = _infer_self(inspect.stack()) 52 | balance = self.balance[self.address] 53 | logging.info("Suicide balance of %s from %s to %s" % (balance, self.address, address)) 54 | self.txs.append((address, balance, 0, 0)) 55 | self.balance[self.address] = 0 56 | 57 | log = logging.info 58 | 59 | class Block(object): 60 | 61 | def __init__(self, timestamp=0, difficulty= 2 ** 22, number=1, parenthash="parenthash"): 62 | self.timestamp = timestamp 63 | self.difficulty = difficulty 64 | self.number = number 65 | self.parenthash = parenthash 66 | self._storages = defaultdict(Storage) 67 | self.gaslimit = 1 * 10 ^ 42 68 | 69 | @property 70 | def basefee(self): 71 | return 1 72 | 73 | # def contract_storage(self, key): # :( 74 | # if _is_called_by_contract(): 75 | # logging.debug("Accessing contract_storage '%s'" % key) 76 | # return self._storages[key] 77 | 78 | 79 | class Stop(RuntimeError): 80 | pass 81 | 82 | 83 | class Contract(object): 84 | 85 | @property 86 | def address(self): 87 | return hex(id(self)) 88 | 89 | @property 90 | def contract(self): 91 | return self 92 | 93 | def __init__(self, *args, **kwargs): 94 | self.storage = Storage() 95 | self.balance = Balance() # balances if balances else defaultdict(int) 96 | self.txs = [] 97 | 98 | caller_module = _infer_self(offset=1) 99 | 100 | # initializing constants 101 | for (arg, value) in kwargs.iteritems(): 102 | if not arg.isupper(): 103 | raise KeyError("Constant '%s' should be uppercase" % arg) 104 | 105 | logging.debug("Initializing constant %s = %s" % (arg, value)) 106 | setattr(caller_module, arg, value) 107 | _modify_frame_global(arg, value) 108 | 109 | def run(self, tx, msg, contract, block): 110 | raise NotImplementedError("Should have implemented this") 111 | 112 | def load(self, script, tx, contract, block): 113 | if hasattr(self, "closure_module") and inspect.ismodule(self.closure_module): 114 | closure = self.closure 115 | closure_module = self.closure_module 116 | else: 117 | log("Loading %s" % script) 118 | 119 | closure = """ 120 | from sim import Block, Contract, Simulation, Tx, Msg, log, stop, suicide, array, stopret, mkmsg, send 121 | class HLL(Contract): 122 | def run(self, tx, msg, contract, block): 123 | """ 124 | baseindent = " " 125 | 126 | with open(script) as fp: 127 | for i, line in enumerate(fp): 128 | # Use comments for stop and log messages 129 | l = line.strip() 130 | if l.startswith("stop"): 131 | # Line number as default stop message 132 | s = "line " + str(i) 133 | if '//' in line: 134 | s = l.split("//")[1].strip() 135 | if not s.startswith('"'): 136 | s = '"' + s + '"' 137 | line = line.split("stop")[0] + "stop(%s)\n" % s 138 | elif "define" in line: 139 | sp = l.split("//") 140 | s = sp[1].strip() 141 | r = s.split("define")[1].strip().split("=") 142 | indent = " " * (len(line) - len(line.lstrip())) 143 | line = indent 144 | if l.split("//")[0].strip().endswith(":"): 145 | line += " " 146 | line += "log('@ line %d: %s" % (i, s) 147 | r[1] = str(r[1]) 148 | if isinstance(r[0], str): 149 | line += ", %%s as hex: 0x%%s' %% (%s," % r[1] 150 | line += "%s.encode('hex')) + " % r[1] 151 | line += "', as int: %d' % " 152 | line += "int(%s.encode('hex'), 16))\n" % r[1] 153 | else: 154 | line += "')\n" 155 | line += baseindent + indent + sp[0].replace(r[0].strip(), r[1].strip(), 1) + "\n" 156 | elif "//" in line: 157 | s = l.split("//")[1].strip() 158 | if s.startswith('"'): 159 | s = "log('@ line %d: ' + %s)\n" % (i, s) 160 | else: 161 | s = "log('@ line %d: %s')\n" % (i, s) 162 | line = line.split("//")[0] + "\n" 163 | if l.split("//")[0].strip().endswith(":"): 164 | line += " " 165 | indent = " " * (len(line) - len(line.lstrip())) 166 | line += baseindent + indent + s 167 | 168 | # Indent 169 | closure += baseindent + line 170 | 171 | # Exponents 172 | closure = closure.replace("^", "**") 173 | 174 | # Hex 175 | closure = closure.replace("hex(", "str(") 176 | 177 | # Return 178 | closure = closure.replace("return(", "stopret(") 179 | 180 | # msg 181 | closure = closure.replace("msg(", "mkmsg(") 182 | 183 | # Comments 184 | closure = closure.replace("//", "#") 185 | 186 | # Initialize module 187 | closure_module = imp.new_module('hll') 188 | 189 | # Pass txs and constants 190 | for i, c in self.__dict__.items(): 191 | closure_module.__dict__[i] = c 192 | 193 | # Set self.closure_module for reuse and self.closure for export 194 | self.closure = closure 195 | self.closure_module = closure_module 196 | 197 | # Execute and run 198 | exec(closure, closure_module.__dict__) 199 | 200 | h = closure_module.HLL() 201 | msg = Msg(tx) 202 | h.run(tx, msg, contract, block) 203 | 204 | 205 | class Simulation(object): 206 | 207 | def __init__(self): 208 | self.log = logging.info 209 | self.warn = logging.warn 210 | self.error = logging.error 211 | 212 | def run_all(self): 213 | test_methods = [(name, method, method.im_func.func_code.co_firstlineno) for name, method in inspect.getmembers(self, predicate=inspect.ismethod) 214 | if name.startswith('test_')] 215 | 216 | # sort by linenr 217 | for name, method, linenr in sorted(test_methods, key=itemgetter(2)): 218 | method() 219 | 220 | def run(self, tx, contract, block=None, method_name=None): 221 | self.stopped = False 222 | if block is None: 223 | block = Block() 224 | 225 | if method_name is None: 226 | method_name = inspect.stack()[1][3] 227 | 228 | logging.info("RUN %s: %s" % (method_name.replace('_', ' ').capitalize(), tx)) 229 | 230 | msg = Msg(tx) 231 | contract.txs = [] 232 | 233 | try: 234 | contract.run(tx, contract, block) 235 | except Stop as e: 236 | if e.message: 237 | logging.warn("Stopped: %s" % e.message) 238 | self.stopped = e.message 239 | else: 240 | logging.info("Stopped") 241 | self.stopped = True 242 | 243 | gas = Gas() 244 | totalgas = gas.calculate_gas(contract) 245 | logging.debug("Fees: %d" % totalgas['total']) 246 | endowment = contract.balance[contract.address] 247 | addendowment = tx.value - totalgas['total'] 248 | if addendowment > 0: 249 | logging.info("Adding %d to endowment" % addendowment) 250 | endowment += addendowment 251 | contract.balance[contract.address] = endowment 252 | logging.info("New endowment: %d" % endowment) 253 | else: 254 | logging.info("Current endowment: %d" % endowment) 255 | # contract.balance[tx.sender] -= endowment 256 | 257 | logging.info('-' * 20) 258 | logging.info(contract.storage) 259 | logging.info('-' * 20) 260 | logging.info(contract.balance) 261 | logging.info('=' * 20) 262 | 263 | 264 | class Storage(object): 265 | 266 | def __init__(self): 267 | self._storage = defaultdict(int) 268 | 269 | def __getitem__(self, key): 270 | if isinstance(key,(str,unicode)): 271 | key = int(key.encode('hex'), 16) 272 | if _is_called_by_contract(): 273 | logging.debug("Accessing storage '%s'" % key) 274 | return self._storage[key] 275 | 276 | def __setitem__(self, key, value): 277 | if isinstance(key,(str,unicode)): 278 | key = int(key.encode('hex'), 16) 279 | if _is_called_by_contract(): 280 | logging.debug("Setting storage '%s' to '%s'" % (key, value)) 281 | self._storage[key] = value 282 | 283 | def __repr__(self): 284 | return "" % repr(self._storage) 285 | 286 | 287 | class Tx(object): 288 | 289 | def __init__(self, sender=None, value=0, fee=1 * 10 ** 15, gas=0, gasprice=0, data=[]): 290 | self.sender = sender 291 | self.value = value 292 | self.fee = fee 293 | self.gasprice = gasprice if gasprice else MINGASPRICE 294 | self.gas = gas if gas else self.fee / self.gasprice 295 | gasfee = self.gas * self.gasprice 296 | if gasfee > self.fee: 297 | self.fee = gasfee 298 | self.data = data 299 | self.datan = len(data) 300 | 301 | contract = _is_called_by_contract() 302 | if contract: 303 | gas = Gas() 304 | totalgas = gas.calculate_gas(contract) 305 | freeload = value + totalgas 306 | self.contract.balance[self.contract.address] += freeload 307 | logging.debug("Freeloading %s with %d" % (sender, freeload)) 308 | 309 | def __repr__(self): 310 | return '' % (self.sender, self.value, self.fee, self.gas, self.gasprice, self.data, self.datan) 311 | 312 | 313 | class Msg(object): 314 | 315 | def __init__(self, tx): 316 | self.datasize = tx.datan 317 | self.sender = tx.sender 318 | self.value = tx.value 319 | self.data = tx.data 320 | 321 | def __getitem__(self): 322 | return self 323 | 324 | 325 | class Balance(object): 326 | 327 | def __init__(self): 328 | self._balance = defaultdict(int) 329 | 330 | def __getitem__(self, address): 331 | balance = self._balance[address] 332 | if _is_called_by_contract(): 333 | logging.debug("Accessing balance '%s' of '%s'" % (balance, address)) 334 | return balance 335 | 336 | def __setitem__(self, address, value): 337 | # if _is_called_by_contract(): 338 | # logging.debug("Cannot set balance of '%s' to '%s'" % (address, value)) 339 | # else: 340 | logging.debug("Setting balance of '%s' to '%s'" % (address, value)) 341 | self._balance[address] = value 342 | 343 | def __repr__(self): 344 | return "" % repr(self._balance) 345 | 346 | class Gas(object): 347 | 348 | def __init__(self): 349 | self._gas = 0 350 | self.gas = 100 351 | self.gasprice = MINGASPRICE 352 | self.pricestep = 1 # opcode count 353 | self.pricedata = 20 # storage load access 354 | self.pricestorage = 100 # storage store access 355 | self.pricememory = 1 # opcode count 356 | self.pricetx = 100 # tx fee 357 | self.pricebasecontract = 100 # new contract feww 358 | self.pricetxdata = 1 # opcode count 359 | 360 | # tx fee + newcontractfee (opt) + stepfee (count opcode steps) + datafee (storage access) + ... 361 | 362 | # quick ref from c++ 363 | # u256 const eth::c_stepGas = 1; 364 | # u256 const eth::c_balanceGas = 20; 365 | # u256 const eth::c_sha3Gas = 20; 366 | # u256 const eth::c_sloadGas = 20; 367 | # u256 const eth::c_sstoreGas = 100; 368 | # u256 const eth::c_createGas = 100; 369 | # u256 const eth::c_callGas = 20; 370 | # u256 const eth::c_memoryGas = 1; 371 | # u256 const eth::c_txDataGas = 5; 372 | 373 | 374 | def calculate_gas(self, contract): 375 | 376 | # if 'traceback' in self.results['es']: 377 | # return 378 | 379 | gas = { 'tx': self.gasprice * self.pricetx, 'step': 0, 'storage': 0 } 380 | 381 | # step gas 382 | # comp_steps = [e for e in self.results['es']['code'] if isinstance(e,str)] 383 | # fees['step'] = len(comp_steps[16:]) 384 | 385 | 386 | # storage fees 387 | code_lines = contract.closure.split('\n') 388 | for i, line in enumerate(code_lines): 389 | if line.startswith('contract.storage['): # TODO: use regex? 390 | fees['storage'] += self.pricestorage 391 | 392 | # add up gas for total 393 | gas['total'] = sum(gas.values()) 394 | 395 | return gas 396 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import imp 5 | import inspect 6 | import logging 7 | import os.path 8 | import sys 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) 11 | 12 | from sim import Simulation 13 | 14 | def get_subclasses(mod, cls): 15 | """Yield the classes in module ``mod`` that inherit from ``cls``""" 16 | for name, obj in inspect.getmembers(mod): 17 | if hasattr(obj, "__bases__") and cls in obj.__bases__: 18 | yield obj 19 | 20 | def load_simulation_class(script): 21 | sim_name = os.path.splitext(os.path.basename(script))[0] 22 | sim_module = imp.load_source(sim_name, script) 23 | 24 | sims = list(get_subclasses(sim_module, Simulation)) 25 | if len(sims) < 1: 26 | raise RuntimeError("No Simulation found in %s" % script) 27 | elif len(sims) > 1: 28 | raise RuntimeError("Multiple Simulations found in %s" % script) 29 | 30 | return sims[0] 31 | 32 | def main(script): 33 | logging.basicConfig(format='%(module)-12s %(levelname)-8s%(message)s', 34 | level=logging.DEBUG) 35 | 36 | simulation_class = load_simulation_class(script) 37 | simulation = simulation_class() 38 | simulation.run_all() 39 | 40 | if __name__ == '__main__': 41 | parser = argparse.ArgumentParser() 42 | parser.add_argument("script") 43 | args = parser.parse_args() 44 | main(args.script) 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E226,E241,E242,E302,E501 3 | max-line-length = 160 4 | --------------------------------------------------------------------------------