Files: 61537ea49c1a1f22f2d8f0ef263fdbef5f2d5e7d / simulate.js
9355 bytesRaw
1 | var states = require('./state') |
2 | var RNG = require('rng') |
3 | var deepEqual = require('deep-equal') |
4 | var invariants = require('./invariants') |
5 | //state is {source, sink, nodeState, log, old_length} |
6 | |
7 | var events = exports.events = function (pState) { |
8 | var ev = [] |
9 | //database changing state, for the peer. |
10 | if(pState.emit) |
11 | ev.push({name: 'append', peer: pState.id}) |
12 | |
13 | //otherwise, normal states can change, |
14 | //except ORDERED states are blocked by the database op. |
15 | for(var k in pState.connections) { |
16 | var cState = pState.connections[k] |
17 | if(!pState.emit && (cState.source.length || isMessage(cState.nodeState.effect))) |
18 | ev.push({name: 'ordered', peer: pState.id, connection: k}) |
19 | if(cState.nodeState.ready != null) |
20 | ev.push({name: 'send', peer: pState.id, connection: k}) |
21 | if(isNote(cState.nodeState.effect)) |
22 | ev.push({name: 'get', peer: pState.id, connection: k}) |
23 | } |
24 | return ev |
25 | } |
26 | |
27 | var model2 = exports.model2 = function (pState, cState, key) { |
28 | /* |
29 | we want to handle appends (and thus validation) before handling any receives. |
30 | we can handle get and send in parallel though. |
31 | |
32 | */ |
33 | |
34 | invariants(cState) |
35 | |
36 | if(key === 'send') { |
37 | var data = cState.nodeState.ready |
38 | cState.sink.push(data) |
39 | cState.nodeState = states.read(cState.nodeState) |
40 | } |
41 | else if(key === 'get') { |
42 | var msg = pState.log[cState.nodeState.effect - 1] //shared |
43 | cState.nodeState.effect = null |
44 | cState.nodeState = states.gotMessage(cState.nodeState, msg) |
45 | } |
46 | else if(key == 'ordered'){ |
47 | //can only be processed if pstate.emit is null |
48 | if(isMessage(cState.nodeState.effect)) { |
49 | pState.emit = cState.nodeState.effect |
50 | //pState.log.push(pState.emit) //shared |
51 | cState.nodeState.effect = null |
52 | } |
53 | else if(cState.source.length) { |
54 | var data = cState.source.shift() //connection. |
55 | if(data == null) throw new Error('should never read null/undefined from network') |
56 | cState.nodeState = (isMessage(data) ? states.receiveMessage : states.receiveNote)(cState.nodeState, data) |
57 | } |
58 | else throw new Error('should not have ran out of options') |
59 | } |
60 | |
61 | invariants(cState) |
62 | |
63 | return [pState, cState] |
64 | } |
65 | |
66 | function isMessage(data) { return data && 'object' === typeof data } |
67 | function isNote(n) { return Number.isInteger(n) } |
68 | |
69 | exports.peer = function peer (network, id, log) { |
70 | if(!log) log = [] |
71 | |
72 | network[id] = { |
73 | id: id, log: log, emit: false, |
74 | connections: {} |
75 | } |
76 | |
77 | return network |
78 | } |
79 | |
80 | exports.connection = function connection (network, from, to) { |
81 | if(!network[from]) throw new Error('from peer:'+ from + ' does not exist') |
82 | if(!network[to]) throw new Error('from peer:'+ to + ' does not exist') |
83 | var ab = [], ba = [] |
84 | network[from].connections[to] = { |
85 | source: ab, sink: ba, nodeState: states.init(network[from].log.length), |
86 | id: from, remote: to, |
87 | } |
88 | network[to].connections[from] = { |
89 | source: ba, sink: ab, nodeState: states.init(network[to].log.length), |
90 | id: to, remote: from, |
91 | } |
92 | return network |
93 | } |
94 | |
95 | exports.countConnections = function countConnections (network) { |
96 | //count unidirectional connections then divide by 2 to get duplex connections |
97 | var uniplex = 0 |
98 | for(var k in network) { |
99 | uniplex += Object.keys(network[k].connections).length |
100 | } |
101 | return uniplex / 2 |
102 | } |
103 | |
104 | exports.isConsistent = function isConsistent (network) { |
105 | //this is the "handshaking lemma" if I recall |
106 | var keys = Object.keys(network) |
107 | for(var i = 0; i < keys.length; i++) { |
108 | var k = keys[i] |
109 | for(var j = i+1; j < keys.length; j++) { |
110 | var l = keys[j] |
111 | if(!deepEqual(network[k].log, network[l].log, 'peer:'+k +' is consistent with :'+j)) |
112 | return false |
113 | } |
114 | } |
115 | return true |
116 | } |
117 | |
118 | function allEvents (network) { |
119 | var evs = [] |
120 | for(var k in network) |
121 | evs = evs.concat(events(network[k])) |
122 | return evs |
123 | } |
124 | |
125 | exports.evolveNetwork = function evolveNetwork (network, msglog, seed) { |
126 | var eventlog = [] |
127 | var rng = new RNG.MT(seed) |
128 | |
129 | function random () { |
130 | return rng.random() |
131 | } |
132 | |
133 | function randomValue(obj) { |
134 | var k = Object.keys(obj) |
135 | var rk = k[~~(random()*k.length)] |
136 | return obj[rk] |
137 | } |
138 | |
139 | var evs |
140 | var N = 1, choice = [], base = [] |
141 | while((evs = allEvents(network)).length) { |
142 | base.push(evs.length) |
143 | var r = ~~(evs.length*random()) |
144 | choice.push(r) |
145 | N = N*evs.length + r |
146 | |
147 | var event = evs[r] |
148 | var pState = network[event.peer] |
149 | eventlog.push(event) |
150 | if(event.name == 'append') { |
151 | var msg = pState.emit |
152 | pState.emit = null |
153 | |
154 | if(msg.sequence > pState.log.length) { |
155 | pState.log.push(msg) |
156 | for(var k in pState.connections) { |
157 | invariants(pState.connections[k]) |
158 | pState.connections[k].nodeState = states.appendMessage(pState.connections[k].nodeState, msg) |
159 | invariants(pState.connections[k]) |
160 | } |
161 | |
162 | } |
163 | |
164 | } |
165 | else { |
166 | var cState = pState.connections[event.connection] |
167 | invariants(cState) |
168 | |
169 | var s = model2(pState, cState, event.name) |
170 | invariants(s[1]) |
171 | pState = s[0]; cState = s[1]; |
172 | if(cState.nodeState.error) { |
173 | console.log(JSON.stringify(pState, null, 2)) |
174 | console.log('evs', evs, event) |
175 | console.log('msglog', msglog) |
176 | console.log('eventlog', eventlog) |
177 | throw new Error('error state') |
178 | } |
179 | if(isMessage(cState.effect)) { |
180 | if(pState.emit) throw new Error('something already appening') |
181 | pState.emit = cState.effect |
182 | cState.effect = null |
183 | } |
184 | } |
185 | } |
186 | return network |
187 | } |
188 | |
189 | |
190 | exports.runner = function (seed, run) { |
191 | var tape = require('tape') |
192 | var max = 1000 |
193 | |
194 | if(seed) |
195 | tape('run 3 message test with 2 peers, seed:'+ (+seed), function (t) { |
196 | run(t, +seed) |
197 | t.end() |
198 | }) |
199 | else |
200 | //running each test is O(Number of tests!) |
201 | tape('run 3 message test with 3 peers, seeds'+0+'-'+max, function (t) { |
202 | for(var i = 0; i < max; i++) (function (i) { |
203 | if(!(i%100)) console.log('seed:'+i) |
204 | try { |
205 | run(t, i) |
206 | } catch(err) { |
207 | console.log('error on seed:'+i) |
208 | throw err |
209 | } |
210 | })(i) |
211 | t.end() |
212 | }) |
213 | |
214 | } |
215 | |
216 | //test 3 peers fully connected, so that some messages get sent twice |
217 | //these connections should get turned off. |
218 | |
219 | function createLogs(n) { |
220 | var log = [] |
221 | for(var i = 0; i < n; i++) |
222 | log.push({author: 'a', sequence: i+1, content: 'hi:'+i.toString(36)}) |
223 | return log |
224 | } |
225 | |
226 | function createPeers(network, N) { |
227 | for(var i = 0; i < N; i++) { |
228 | var id = String.fromCharCode('A'.charCodeAt(0) + i) |
229 | network = exports.peer(network, id, []) |
230 | } |
231 | return network |
232 | } |
233 | |
234 | function createConnections (network, list) { |
235 | list.split(',').map(function (ab) { |
236 | if(ab) |
237 | network = exports.connection(network, ab[0], ab[1]) |
238 | }) |
239 | return network |
240 | } |
241 | |
242 | |
243 | var letters = 'abcdefghijklmnopqrstuzwxyz'.toUpperCase() |
244 | //create random network with N nodes and E edges |
245 | exports.createRandomNetwork = function createRandomNetwork(N, E, seed) { |
246 | |
247 | if(E < N -1) throw new Error( |
248 | 'not enough edges:'+E+ |
249 | ' to connect a network with:'+N +' nodes. '+ |
250 | 'At least '+N-1+' are required.' |
251 | ) |
252 | |
253 | var rng = new RNG.MT(seed) |
254 | |
255 | function random() { |
256 | return rng.random() |
257 | } |
258 | var network = {} |
259 | network = exports.peer(network, 'A') |
260 | |
261 | //create N-1 more peers |
262 | //connect them randomly (but gaurantee a connected graph) |
263 | for(var i = 1; i < N; i++) { |
264 | var me = letters[i] |
265 | network = exports.peer(network, me, []) |
266 | //at least one connection to a peer currently in the network, gaurantees a connected network. |
267 | network = exports.connection(network, me, letters[~~((i-1)*random())]) |
268 | } |
269 | |
270 | for(var i = 0; i < E-N; i++) { |
271 | var me = letters[i%N] |
272 | var other |
273 | while(me == (other = letters[~~(random()*N)])) |
274 | ; |
275 | network = exports.connection(network, me, other) |
276 | } |
277 | |
278 | return network |
279 | } |
280 | |
281 | exports.createSimulation = function (M, N, C) { |
282 | var network = {} |
283 | var a_log = createLogs(M) |
284 | network = createPeers(network, N) |
285 | network.A.log = a_log //first peer is always alice |
286 | return createConnections(network, C) |
287 | } |
288 | |
289 | //run a test with M messages over N peers, returning |
290 | exports.basic = function (createNetwork) { |
291 | return function (t, seed) { |
292 | var msglog = [] |
293 | |
294 | var network = createNetwork(seed) |
295 | |
296 | network = exports.evolveNetwork(network, msglog, seed) |
297 | |
298 | if(!exports.isConsistent(network)) |
299 | throw new Error('network not consistent') |
300 | |
301 | //add one more item to A's log |
302 | |
303 | network.A.emit = {author: 'a', sequence: network.A.log.length+1, content: 'LIVE'} |
304 | |
305 | network = exports.evolveNetwork(network, msglog, seed*2) |
306 | if(!exports.isConsistent(network)) |
307 | throw new Error('network not consistent') |
308 | //todo: make this a processable event log thing |
309 | network.A.emit = {author: 'a', sequence: network.A.log.length+1, content: 'LIVE'} |
310 | |
311 | network = exports.evolveNetwork(network, msglog, seed*2) |
312 | if(!exports.isConsistent(network)) |
313 | throw new Error('network not consistent') |
314 | |
315 | return msglog |
316 | } |
317 | } |
318 | |
319 | function print_network (network) { |
320 | for(var k in network) { |
321 | var s = k + ': ' |
322 | for(var j in network) { |
323 | s += k == j ? ' ' : network[k].connections[j] ? j : '.' |
324 | } |
325 | console.log(s) |
326 | } |
327 | } |
328 | |
329 | function isConnected (network) { |
330 | var reach = {} |
331 | ;(function search (k) { |
332 | reach[k] = true |
333 | for(var j in network[k].connections) |
334 | if(!reach[j]) search(j) |
335 | }(Object.keys(network)[0])) |
336 | |
337 | return Object.keys(network).length === Object.keys(reach).length |
338 | } |
339 | |
340 |
Built with git-ssb-web