├── 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 |
--------------------------------------------------------------------------------