├── README.md ├── contractPay.sol ├── contractSprite.sol ├── highlight.png ├── preimageManager.sol ├── test_pay.py └── test_sprites.py /README.md: -------------------------------------------------------------------------------- 1 | Sprites - Payment Channels that Go Faster than Lightning 2 | ======= 3 | 4 | Once a payment channel is established between a pair of parties, they can pay each other very rapidly using only off-chain messages (unless one of them crashes, in which case the other can go on-chain to reclaim their balance). 5 | 6 | The Lightning Network (and the Raiden Network) also support a "conditional payment" feature that lets one party pay another across a *path* of payment channels, even if they do not have a payment channel directly established between each other. The conditional payment works by reserving a portion of the payment channel's collateral, such that in case of dispute it can be claimed by revealing the preimage of some hash before a deadline. However, to make sure the payment is atomic and honest parties do not risk losing money, the deadline has to be larger and larger the longer the path. 7 | 8 | Sprites are a new kind of payment channel that avoid this problem. They make use of a global contract, the "PreimageManager", which acts like a global condition for all the payment channels on the contract. 9 | 10 | 11 | 12 | This github repo provides a simple implementation of basic bidirectional payment channels (`contractPay.sol`), and an extension that supports conditional payments as described above (`contractSprite.sol`). 13 | 14 | Requirements 15 | ---- 16 | - pyethereum 17 | - solc 18 | 19 | These test scripts simply exercise the function of the contract. Coming soon: extended test cases handling linked payments. 20 | ``` 21 | python test_pay.py 22 | ``` 23 | or 24 | ``` 25 | python test_sprites.sol 26 | ``` 27 | 28 | Contents 29 | ------- 30 | - contractPay.sol, test_pay.sol: 31 | Simple bidirectional payment channel, based on the state channel pattern. 32 | 33 | Some simplifications: 34 | - while state channels can continue receiving on-chain input, contractDuplex reaches a terminal state after one trigger 35 | - withdrawals are encoded as monotonically increasing values, so no need to explicitly trigger an outputs 36 | 37 | - contractSprite.sol, test_sprite.sol: 38 | A duplex channel with conditional (hashed timelock) payments, but that supports constant expiry times 39 | 40 | -------------------------------------------------------------------------------- /contractPay.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.3; 2 | 3 | // Note: Initial version does NOT support concurrent conditional payments! 4 | 5 | contract PaymentChannel { 6 | 7 | // Blocks for grace period 8 | uint constant DELTA = 10; 9 | 10 | // Events 11 | event EventInit(); 12 | event EventUpdate(int r); 13 | event LogBytes32(bytes32 b); 14 | event EventPending(uint T1, uint T2); 15 | 16 | // Utility functions 17 | modifier after_ (uint T) { if (T > 0 && block.number >= T) _; else throw; } 18 | modifier before(uint T) { if (T == 0 || block.number < T) _; else throw; } 19 | modifier onlyplayers { if (playermap[msg.sender] > 0) _; else throw; } 20 | function assert(bool b) internal { if (!b) throw; } 21 | function max(uint a, uint b) internal returns(uint) { if (a>b) return a; else return b; } 22 | function min(uint a, uint b) internal returns(uint) { if (a uint) playermap; 38 | 39 | ///////////////////////////////////// 40 | // Payment Channel - Application specific data 41 | //////////////////////////////////// 42 | 43 | // State channel states 44 | int [2] public credits; 45 | uint[2] public withdrawals; 46 | 47 | // Externally affected states 48 | uint[2] public deposits; // Monotonic, only incremented by deposit() function 49 | uint[2] public withdrawn; // Monotonic, only incremented by withdraw() function 50 | 51 | function sha3int(int r) constant returns(bytes32) { 52 | return sha3(r); 53 | } 54 | 55 | function PaymentChannel(address[2] _players) { 56 | for (uint i = 0; i < 2; i++) { 57 | players[i] = _players[i]; 58 | playermap[_players[i]] = i + 1; 59 | } 60 | EventInit(); 61 | } 62 | 63 | // Increment on new deposit 64 | function deposit() payable onlyplayers { 65 | deposits[playermap[msg.sender]-1] += msg.value; 66 | } 67 | 68 | // Increment on withdrawal 69 | function withdraw() onlyplayers { 70 | uint i = playermap[msg.sender]-1; 71 | uint toWithdraw = withdrawals[i] - withdrawn[i]; 72 | withdrawn[i] = withdrawals[i]; 73 | assert(msg.sender.send(toWithdraw)); 74 | } 75 | 76 | // State channel update function 77 | function update(uint[3] sig, int r, int[2] _credits, uint[2] _withdrawals) 78 | onlyplayers { 79 | 80 | // Only update to states with larger round number 81 | if (r <= bestRound) return; 82 | 83 | // Check the signature of the other party 84 | uint i = (3 - playermap[msg.sender]) - 1; 85 | var _h = sha3(r, _credits, _withdrawals); 86 | var V = uint8 (sig[0]); 87 | var R = bytes32(sig[1]); 88 | var S = bytes32(sig[2]); 89 | verifySignature(players[i], _h, V, R, S); 90 | 91 | // Update the state 92 | credits[0] = _credits[0]; 93 | credits[1] = _credits[1]; 94 | withdrawals[0] = _withdrawals[0]; 95 | withdrawals[1] = _withdrawals[1]; 96 | bestRound = r; 97 | EventUpdate(r); 98 | } 99 | 100 | // Causes a timeout for the finalize time 101 | function trigger() onlyplayers { 102 | assert( status == Status.OK ); 103 | status = Status.PENDING; 104 | deadline = block.number + DELTA; // Set the deadline for collecting inputs or updates 105 | EventPending(block.number, deadline); 106 | } 107 | 108 | function finalize() { 109 | assert( status == Status.PENDING ); 110 | assert( block.number > deadline ); 111 | 112 | // Note: Is idempotent, may be called multiple times 113 | 114 | // Withdraw the maximum amount of money 115 | withdrawals[0] += uint(int(deposits[0]) + credits[0]); 116 | withdrawals[1] += uint(int(deposits[1]) + credits[1]); 117 | credits[0] = -int(deposits[0]); 118 | credits[1] = -int(deposits[1]); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /contractSprite.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.3; 2 | 3 | // External interface 4 | contract PreimageManager { 5 | function submitPreimage(bytes32 x) {} 6 | function revealedBefore(bytes32 h, uint T) returns(bool) {} 7 | } 8 | 9 | // Note: Initial version does NOT support concurrent conditional payments! 10 | 11 | contract SpriteChannel { 12 | 13 | // Blocks for grace period 14 | uint constant DELTA = 10; 15 | 16 | // Events 17 | event EventInit(); 18 | event EventUpdate(int r); 19 | event LogBytes32(bytes32 b); 20 | event EventPending(uint T1, uint T2); 21 | 22 | // Utility functions 23 | modifier after_ (uint T) { if (T > 0 && block.number >= T) _; else throw; } 24 | modifier before(uint T) { if (T == 0 || block.number < T) _; else throw; } 25 | modifier onlyplayers { if (playermap[msg.sender] > 0) _; else throw; } 26 | function assert(bool b) internal { if (!b) throw; } 27 | function max(uint a, uint b) internal returns(uint) { if (a>b) return a; else return b; } 28 | function min(uint a, uint b) internal returns(uint) { if (a uint) playermap; 44 | PreimageManager pm; 45 | 46 | ///////////////////////////////////// 47 | // Sprite - Application specific data 48 | //////////////////////////////////// 49 | 50 | // State channel states 51 | int [2] public credits; 52 | uint[2] public withdrawals; 53 | 54 | // Conditional payment 55 | // NOTE: for simplicity, only one conditional payment supported, L to R 56 | bytes32 public hash; 57 | uint public expiry; 58 | uint public amount; 59 | 60 | // Externally affected states 61 | uint[2] public deposits; // Monotonic, only incremented by deposit() function 62 | uint[2] public withdrawn; // Monotonic, only incremented by withdraw() function 63 | 64 | function sha3int(int r) constant returns(bytes32) { 65 | return sha3(r); 66 | } 67 | 68 | function SpriteChannel(PreimageManager _pm, address[2] _players) { 69 | pm = _pm; 70 | for (uint i = 0; i < 2; i++) { 71 | players[i] = _players[i]; 72 | playermap[_players[i]] = i + 1; 73 | } 74 | EventInit(); 75 | } 76 | 77 | // Increment on new deposit 78 | function deposit() payable onlyplayers { 79 | deposits[playermap[msg.sender]-1] += msg.value; 80 | } 81 | 82 | // Increment on withdrawal 83 | function withdraw() onlyplayers { 84 | uint i = playermap[msg.sender]-1; 85 | uint toWithdraw = withdrawals[i] - withdrawn[i]; 86 | withdrawn[i] = withdrawals[i]; 87 | assert(msg.sender.send(toWithdraw)); 88 | } 89 | 90 | // State channel update function 91 | function update(uint[3] sig, int r, int[2] _credits, uint[2] _withdrawals, 92 | bytes32 _hash, uint _expiry, uint _amount) 93 | onlyplayers { 94 | 95 | // Only update to states with larger round number 96 | if (r <= bestRound) return; 97 | 98 | // Check the signature of the other party 99 | uint i = (3 - playermap[msg.sender]) - 1; 100 | var _h = sha3(r, _credits, _withdrawals, _hash, _expiry, _amount); 101 | var V = uint8 (sig[0]); 102 | var R = bytes32(sig[1]); 103 | var S = bytes32(sig[2]); 104 | verifySignature(players[i], _h, V, R, S); 105 | 106 | // Update the state 107 | credits[0] = _credits[0]; 108 | credits[1] = _credits[1]; 109 | withdrawals[0] = _withdrawals[0]; 110 | withdrawals[1] = _withdrawals[1]; 111 | amount = _amount; 112 | hash = _hash; 113 | expiry = _expiry; 114 | bestRound = r; 115 | EventUpdate(r); 116 | } 117 | 118 | // Causes a timeout for the finalize time 119 | function trigger() onlyplayers { 120 | assert( status == Status.OK ); 121 | status = Status.PENDING; 122 | deadline = block.number + DELTA; // Set the deadline for collecting inputs or updates 123 | EventPending(block.number, deadline); 124 | } 125 | 126 | function finalize() { 127 | assert( status == Status.PENDING ); 128 | assert( block.number > deadline ); 129 | 130 | // Finalize is safe to call multiple times 131 | // If "trigger" occurs before a hashlock expires, finalize will need to be called again 132 | 133 | if (amount > 0 && block.number > expiry) { 134 | // Completes on-chain 135 | if (pm.revealedBefore(hash, expiry)) 136 | withdrawals[1] += amount; 137 | // Cancels off-chain 138 | else 139 | withdrawals[0] += amount; 140 | amount = 0; 141 | hash = 0; 142 | expiry = 0; 143 | } 144 | 145 | // Withdraw the maximum amount of money 146 | withdrawals[0] += uint(int(deposits[0]) + credits[0]); 147 | withdrawals[1] += uint(int(deposits[1]) + credits[1]); 148 | credits[0] = -int(deposits[0]); 149 | credits[1] = -int(deposits[1]); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiller/sprites/42871992519bb66b958be50c63dedbd8c242fbaa/highlight.png -------------------------------------------------------------------------------- /preimageManager.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.3; 2 | 3 | contract PreimageManager { 4 | mapping ( bytes32 => uint ) timestamp; 5 | function submitPreimage(bytes32 x) { 6 | if (timestamp[sha3(x)] == 0) 7 | timestamp[sha3(x)] = block.number; 8 | } 9 | 10 | function revealedBefore(bytes32 h, uint T) returns(bool) { 11 | uint t = timestamp[h]; 12 | return (t > 0 && t <= T); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test_pay.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | from ethereum import utils 3 | from ethereum._solidity import get_solidity 4 | SOLIDITY_AVAILABLE = get_solidity() is not None 5 | from Crypto.Hash import SHA256 6 | 7 | import bitcoin 8 | 9 | # Logging 10 | from ethereum import slogging 11 | slogging.configure(':INFO,eth.vm:INFO') 12 | #slogging.configure(':DEBUG') 13 | #slogging.configure(':DEBUG,eth.vm:TRACE') 14 | 15 | xor = lambda (x,y): chr(ord(x) ^ ord(y)) 16 | xors = lambda x,y: ''.join(map(xor,zip(x,y))) 17 | zfill = lambda s: (32-len(s))*'\x00' + s 18 | flatten = lambda x: [z for y in x for z in y] 19 | 20 | def int_to_bytes(x): 21 | # pyethereum int to bytes does not handle negative numbers 22 | assert -(1<<255) <= x < (1<<255) 23 | return utils.int_to_bytes((1<<256) + x if x < 0 else x) 24 | 25 | def broadcast(p, r, h, sig): 26 | print 'player[%d]'%p.i, 'broadcasts', r, h.encode('hex'), sig 27 | 28 | def sign(h, priv): 29 | assert len(h) == 32 30 | V, R, S = bitcoin.ecdsa_raw_sign(h, priv) 31 | return V,R,S 32 | 33 | def verify_signature(addr, h, (V,R,S)): 34 | pub = bitcoin.ecdsa_raw_recover(h, (V,R,S)) 35 | pub = bitcoin.encode_pubkey(pub, 'bin') 36 | addr_ = utils.sha3(pub[1:])[12:] 37 | assert addr_ == addr 38 | return True 39 | 40 | def getstatus(): 41 | depositsL = contract.deposits(0) 42 | depositsR = contract.deposits(1) 43 | creditsL = contract.credits(0) 44 | creditsR = contract.credits(1) 45 | wdrawL = contract.withdrawals(0) 46 | wdrawR = contract.withdrawals(1) 47 | print 'Status:', ['OK','PENDING'][contract.status()] 48 | print '[L] deposits:', depositsL, 'credits:', creditsL, 'withdrawals:', wdrawL 49 | print '[R] deposits:', depositsR, 'credits:', creditsR, 'withdrawals:', wdrawR 50 | 51 | 52 | class Player(): 53 | def __init__(self, sk, i, contract): 54 | self.sk = sk 55 | self.i = i 56 | self.contract = contract 57 | self.status = "OK" 58 | self.lastRound = -1 59 | self.lastCommit = None, (0, 0, 0, 0) 60 | self.lastProposed = None 61 | 62 | def deposit(self, amt): 63 | self.contract.deposit(value=amt, sender=self.sk) 64 | 65 | def acceptInputs(self, r, payL, payR, wdrawL, wdrawR): 66 | assert self.status == "OK" 67 | assert r == self.lastRound + 1 68 | # Assumption - don't call acceptInputs(r,...) multiple times 69 | 70 | depositsL = contract.deposits(0); 71 | depositsR = contract.deposits(1); 72 | withdrawalsL = contract.withdrawals(0); 73 | withdrawalsR = contract.withdrawals(1); 74 | 75 | _, (creditsL, creditsR, withdrawnL, withdrawnR) = self.lastCommit 76 | 77 | assert payL <= depositsL + creditsL 78 | assert payR <= depositsR + creditsR 79 | assert wdrawL <= depositsL + creditsL - payL 80 | assert wdrawR <= depositsR + creditsR - payR 81 | 82 | creditsL += payR - payL - wdrawL 83 | creditsR += payL - payR - wdrawR 84 | withdrawalsL += wdrawL 85 | withdrawalsR += wdrawR 86 | 87 | self.lastProposed = (creditsL, creditsR, withdrawalsL, withdrawalsR) 88 | 89 | self.h = utils.sha3(zfill(utils.int_to_bytes(r)) + 90 | zfill(int_to_bytes(creditsL)) + 91 | zfill(int_to_bytes(creditsR)) + 92 | zfill(utils.int_to_bytes(withdrawalsL)) + 93 | zfill(utils.int_to_bytes(withdrawalsR))) 94 | sig = sign(self.h, self.sk) 95 | broadcast(self, r, self.h, sig) 96 | return sig 97 | 98 | def receiveSignatures(self, r, sigs): 99 | assert self.status == "OK" 100 | assert r == self.lastRound + 1 101 | 102 | for i,sig in enumerate(sigs): 103 | verify_signature(addrs[i], self.h, sig) 104 | 105 | self.lastCommit = sigs, self.lastProposed 106 | self.lastRound += 1 107 | 108 | def getstatus(self): 109 | print '[Local view of Player %d]' % self.i 110 | print 'Last round:', self.lastRound 111 | depositsL = contract.deposits(0) 112 | depositsR = contract.deposits(1) 113 | _, (creditsL, creditsR, wdrawL, wdrawR) = self.lastCommit 114 | print 'Status:', self.status 115 | print '[L] deposits:', depositsL, 'credits:', creditsL, 'withdrawals:', wdrawL 116 | print '[R] deposits:', depositsR, 'credits:', creditsR, 'withdrawals:', wdrawR 117 | 118 | 119 | def update(self): 120 | # Place our updated state in the contract 121 | sigs, (creditsL, creditsR, withdrawalsL, withdrawalsR) = self.lastCommit 122 | sig = sigs[1] if self.i == 0 else sigs[0] 123 | self.contract.update(sig, self.lastRound, (creditsL, creditsR), (withdrawalsL, withdrawalsR), sender=self.sk) 124 | 125 | 126 | # Create the simulated blockchain 127 | s = tester.state() 128 | s.mine() 129 | tester.gas_limit = 3141592 130 | 131 | 132 | keys = [tester.k1, 133 | tester.k2] 134 | addrs = map(utils.privtoaddr, keys) 135 | 136 | # Create the contract 137 | contract_code = open('contractPay.sol').read() 138 | contract = s.abi_contract(contract_code, language='solidity', 139 | constructor_parameters= ((addrs[0], addrs[1]),) ) 140 | 141 | 142 | def completeRound(players, r, payL, payR, wdrawL, wdrawR): 143 | sigL = players[0].acceptInputs(r, payL, payR, wdrawL, wdrawR) 144 | sigR = players[1].acceptInputs(r, payL, payR, wdrawL, wdrawR) 145 | sigs = (sigL, sigR) 146 | players[0].receiveSignatures(r, sigs) 147 | players[1].receiveSignatures(r, sigs) 148 | 149 | # Take a snapshot before trying out test cases 150 | #try: s.revert(s.snapshot()) 151 | #except: pass # FIXME: I HAVE NO IDEA WHY THIS IS REQUIRED 152 | s.mine() 153 | base = s.snapshot() 154 | 155 | players = [Player(sk, i, contract) for i,sk in enumerate(keys)] 156 | 157 | def test1(): 158 | # Some test behaviors 159 | getstatus() 160 | players[0].deposit(10) 161 | getstatus() 162 | completeRound(players, 0, 5, 0, 0, 0) 163 | 164 | # Update 165 | players[0].getstatus() 166 | players[0].update() 167 | getstatus() 168 | 169 | # Check some assertions 170 | try: completeRound(players, 1, 6, 0, 0, 0) # Should fail 171 | except AssertionError: pass # Should fail 172 | else: raise ValueError, "Too much balance!" 173 | 174 | completeRound(players, 1, 0, 2, 0, 1) 175 | players[0].getstatus() 176 | 177 | print 'Triggering' 178 | contract.trigger(sender=keys[0]) 179 | players[0].update() 180 | s.mine(15) 181 | 182 | print 'Finalize' 183 | contract.finalize() 184 | getstatus() 185 | 186 | 187 | if __name__ == '__main__': 188 | try: __IPYTHON__ 189 | except NameError: 190 | test1() 191 | -------------------------------------------------------------------------------- /test_sprites.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | from ethereum import utils 3 | from ethereum._solidity import get_solidity 4 | SOLIDITY_AVAILABLE = get_solidity() is not None 5 | from Crypto.Hash import SHA256 6 | import os 7 | import bitcoin 8 | 9 | # Logging 10 | from ethereum import slogging 11 | slogging.configure(':INFO,eth.vm:INFO') 12 | #slogging.configure(':DEBUG') 13 | #slogging.configure(':DEBUG,eth.vm:TRACE') 14 | 15 | xor = lambda (x,y): chr(ord(x) ^ ord(y)) 16 | xors = lambda x,y: ''.join(map(xor,zip(x,y))) 17 | zfill = lambda s: (32-len(s))*'\x00' + s 18 | flatten = lambda x: [z for y in x for z in y] 19 | 20 | def int_to_bytes(x): 21 | # pyethereum int to bytes does not handle negative numbers 22 | assert -(1<<255) <= x < (1<<255) 23 | return utils.int_to_bytes((1<<256) + x if x < 0 else x) 24 | 25 | def broadcast(p, r, h, sig): 26 | print 'player[%d]'%p.i, 'broadcasts', r, h.encode('hex'), sig 27 | 28 | def sign(h, priv): 29 | assert len(h) == 32 30 | V, R, S = bitcoin.ecdsa_raw_sign(h, priv) 31 | return V,R,S 32 | 33 | def verify_signature(addr, h, (V,R,S)): 34 | pub = bitcoin.ecdsa_raw_recover(h, (V,R,S)) 35 | pub = bitcoin.encode_pubkey(pub, 'bin') 36 | addr_ = utils.sha3(pub[1:])[12:] 37 | assert addr_ == addr 38 | return True 39 | 40 | def getstatus(): 41 | depositsL = contract.deposits(0) 42 | depositsR = contract.deposits(1) 43 | creditsL = contract.credits(0) 44 | creditsR = contract.credits(1) 45 | wdrawL = contract.withdrawals(0) 46 | wdrawR = contract.withdrawals(1) 47 | print 'Status:', ['OK','PENDING'][contract.status()] 48 | print '[L] avail:', depositsL + creditsL, '(deposits:', depositsL, 'credits:', creditsL, ') withdrawals:', wdrawL 49 | print '[R] avail:', depositsR + creditsR, '(deposits:', depositsR, 'credits:', creditsR, ') withdrawals:', wdrawR 50 | 51 | class Player(): 52 | def __init__(self, sk, i, PM, contract): 53 | self.sk = sk 54 | self.i = i 55 | self.PM = PM 56 | self.contract = contract 57 | self.status = "OK" 58 | self.lastRound = -1 59 | # credL, credR, wdrawL, wdrawR, hash, expiry, amount 60 | self.lastCommit = None, (0, 0, 0, 0, '', 0, 0) 61 | self.lastProposed = None 62 | 63 | def deposit(self, amt): 64 | self.contract.deposit(value=amt, sender=self.sk) 65 | 66 | def acceptInputs(self, r, payL, payR, wdrawL, wdrawR, cmd): 67 | assert self.status == "OK" 68 | assert r == self.lastRound + 1 69 | # Assumption - don't call acceptInputs(r,...) multiple times 70 | 71 | depositsL = contract.deposits(0); 72 | depositsR = contract.deposits(1); 73 | 74 | _, (creditsL, creditsR, withdrawalsL, withdrawalsR, 75 | h, expiry, amount) = self.lastCommit 76 | 77 | # Code for handling conditional payments 78 | try: 79 | # Opening a new conditional payment 80 | if cmd[0] == 'open': 81 | _h, _expiry, _amount = cmd[1:] 82 | assert amount == 0 # No inflight payment 83 | assert _amount <= depositsL + creditsL # No overpayment 84 | assert _expiry >= s.block.number + 10 85 | h = _h 86 | expiry = _expiry 87 | amount = _amount 88 | creditsL -= _amount # Reserve the amount for the conditional payment 89 | except TypeError, IndexError: 90 | pass 91 | if cmd == 'cancel': 92 | # Should only be invoked with permission from R 93 | assert amount > 0 94 | creditsL += amount 95 | amount = 0 96 | if cmd == 'complete': 97 | # Should only be invoked with permission from L 98 | assert amount > 0 99 | creditsR += amount 100 | amount = 0 101 | 102 | assert payL <= depositsL + creditsL 103 | assert payR <= depositsR + creditsR 104 | assert wdrawL <= depositsL + creditsL - payL 105 | assert wdrawR <= depositsR + creditsR - payR 106 | 107 | creditsL += payR - payL - wdrawL 108 | creditsR += payL - payR - wdrawR 109 | withdrawalsL += wdrawL 110 | withdrawalsR += wdrawR 111 | 112 | self.lastProposed = (creditsL, creditsR, withdrawalsL, withdrawalsR, 113 | h, expiry, amount) 114 | 115 | self.h = utils.sha3(zfill(utils.int_to_bytes(r)) + 116 | zfill(int_to_bytes(creditsL)) + 117 | zfill(int_to_bytes(creditsR)) + 118 | zfill(utils.int_to_bytes(withdrawalsL)) + 119 | zfill(utils.int_to_bytes(withdrawalsR)) + 120 | zfill(h) + 121 | zfill(utils.int_to_bytes(expiry)) + 122 | zfill(utils.int_to_bytes(amount))) 123 | sig = sign(self.h, self.sk) 124 | #broadcast(self, r, self.h, sig) 125 | return sig 126 | 127 | def receiveSignatures(self, r, sigs): 128 | assert self.status == "OK" 129 | assert r == self.lastRound + 1 130 | 131 | for i,sig in enumerate(sigs): 132 | verify_signature(addrs[i], self.h, sig) 133 | 134 | self.lastCommit = sigs, self.lastProposed 135 | self.lastRound += 1 136 | 137 | def submitPreimage(self): 138 | # Need to call this before expiry time, if not already present! 139 | _, (_,_,_,_, h, _,_) = self.lastCommit 140 | assert utils.sha3(self.preimage) == h 141 | self.PM.submitPreimage(self.preimage) 142 | 143 | def getstatus(self): 144 | print '[Local view of Player %d]' % self.i 145 | print 'Last round:', self.lastRound 146 | depositsL = contract.deposits(0) 147 | depositsR = contract.deposits(1) 148 | _, (creditsL, creditsR, wdrawL, wdrawR, h, expiry, amt) = self.lastCommit 149 | print 'Status:', self.status 150 | print '[L] deposits:', depositsL, 'credits:', creditsL, 'withdrawals:', wdrawL 151 | print '[R] deposits:', depositsR, 'credits:', creditsR, 'withdrawals:', wdrawR 152 | print 'h:', h.encode('hex'), 'expiry:', expiry, 'amount:', amt 153 | 154 | def update(self): 155 | # Place our updated state in the contract 156 | sigs, (creditsL, creditsR, withdrawalsL, withdrawalsR, h, expiry, amt) = self.lastCommit 157 | sig = sigs[1] if self.i == 0 else sigs[0] 158 | self.contract.update(sig, self.lastRound, (creditsL, creditsR), (withdrawalsL, withdrawalsR), h, expiry, amt, sender=self.sk) 159 | 160 | # Create the simulated blockchain 161 | s = tester.state() 162 | s.mine() 163 | tester.gas_limit = 3141592 164 | 165 | 166 | keys = [tester.k1, 167 | tester.k2] 168 | addrs = map(utils.privtoaddr, keys) 169 | 170 | # Create the PreimageManager contract 171 | contractPM = s.abi_contract(open('preimageManager.sol').read(), language='solidity') 172 | contract_code = open('contractSprite.sol').read() 173 | 174 | contract = s.abi_contract(contract_code, language='solidity', 175 | constructor_parameters=(contractPM.address, (addrs[0], addrs[1]))) 176 | 177 | players = [Player(sk, i, contractPM, contract) for i,sk in enumerate(keys)] 178 | 179 | def openpayment(players, amount): 180 | x = os.urandom(32) 181 | h = utils.sha3(x) 182 | assert players[0].lastRound == players[1].lastRound 183 | players[0].preimage = x 184 | r = players[0].lastRound + 1 185 | sigL = players[0].acceptInputs(r, 0, 0, 0, 0, ('open', h, s.block.number + 10, amount)) 186 | sigR = players[1].acceptInputs(r, 0, 0, 0, 0, ('open', h, s.block.number + 10, amount)) 187 | sigs = (sigL, sigR) 188 | players[0].receiveSignatures(r, sigs) 189 | players[1].receiveSignatures(r, sigs) 190 | 191 | def completepayment(players): 192 | assert players[0].lastRound == players[1].lastRound 193 | r = players[0].lastRound + 1 194 | sigL = players[0].acceptInputs(r, 0, 0, 0, 0, 'complete') 195 | sigR = players[1].acceptInputs(r, 0, 0, 0 ,0, 'complete') 196 | sigs = (sigL, sigR) 197 | players[0].receiveSignatures(r, sigs) 198 | players[1].receiveSignatures(r, sigs) 199 | 200 | def cancelpayment(players): 201 | assert players[0].lastRound == players[1].lastRound 202 | r = players[0].lastRound + 1 203 | sigL = players[0].acceptInputs(r, 0, 0, 0, 0, 'cancel') 204 | sigR = players[1].acceptInputs(r, 0, 0, 0, 0, 'cancel') 205 | sigs = (sigL, sigR) 206 | players[0].receiveSignatures(r, sigs) 207 | players[1].receiveSignatures(r, sigs) 208 | 209 | def completeRound(players, r, payL, payR, wdrawL, wdrawR): 210 | sigL = players[0].acceptInputs(r, payL, payR, wdrawL, wdrawR, None) 211 | sigR = players[1].acceptInputs(r, payL, payR, wdrawL, wdrawR, None) 212 | sigs = (sigL, sigR) 213 | players[0].receiveSignatures(r, sigs) 214 | players[1].receiveSignatures(r, sigs) 215 | 216 | # Take a snapshot before trying out test cases 217 | #try: s.revert(s.snapshot()) 218 | #except: pass # FIXME: I HAVE NO IDEA WHY THIS IS REQUIRED 219 | s.mine() 220 | base = s.snapshot() 221 | 222 | def test1(): 223 | # Some test behaviors 224 | getstatus() 225 | players[0].deposit(10) 226 | getstatus() 227 | completeRound(players, 0, 5, 0, 0, 0) 228 | 229 | # Update 230 | players[0].getstatus() 231 | players[0].update() 232 | getstatus() 233 | 234 | # Check some assertions 235 | try: completeRound(players, 1, 6, 0, 0, 0) # Should fail 236 | except AssertionError: pass # Should fail 237 | else: raise ValueError, "Too much balance!" 238 | 239 | completeRound(players, 1, 0, 2, 0, 1) 240 | players[0].getstatus() 241 | 242 | print 'Triggering' 243 | contract.trigger(sender=keys[0]) 244 | players[0].update() 245 | s.mine(15) 246 | 247 | print 'Finalize' 248 | contract.finalize() 249 | getstatus() 250 | 251 | def test2(): 252 | # Player 1 deposits 10 253 | # Player 1 transfers 3 254 | # Player 1 makes a conditional payment of 5 255 | # Payment completes 256 | players[0].deposit(10) 257 | print 'Pay(L to R): 3' 258 | completeRound(players, 0, 3, 0, 0, 0) 259 | 260 | print 'Conditional payment: 5' 261 | openpayment(players, 5) 262 | players[0].getstatus() 263 | if 0: 264 | print 'Complete' 265 | completepayment(players) 266 | else: 267 | print 'Cancel' 268 | cancelpayment(players) 269 | getstatus() 270 | 271 | def test3(): 272 | # Player 1 deposits 10 273 | # Player 1 transfers 3 274 | # Player 1 makes a conditional payment of 5 275 | # Payment disputes on-chain 276 | players[0].deposit(10) 277 | print 'Pay(L to R): 3' 278 | completeRound(players, 0, 3, 0, 0, 0) 279 | 280 | print 'Conditional payment: 5' 281 | openpayment(players, 5) 282 | players[0].getstatus() 283 | 284 | if 1: 285 | 'Submitting preimage' 286 | players[0].submitPreimage() 287 | players[0].update() 288 | contract.trigger(sender=keys[0]) 289 | s.mine(15) 290 | print 'Finalize' 291 | contract.finalize() 292 | getstatus() 293 | 294 | if __name__ == '__main__': 295 | try: __IPYTHON__ 296 | except NameError: 297 | test3() 298 | 299 | --------------------------------------------------------------------------------