1 module tagion.testbench.services.double_spend;
2 // Default import list for bdd
3 import core.thread;
4 import core.time;
5 import std.algorithm;
6 import std.concurrency : thisTid;
7 import std.format;
8 import std.range;
9 import std.stdio;
10 import std.typecons : Tuple;
11 import tagion.actor;
12 import tagion.behaviour;
13 import tagion.communication.HiRPC;
14 import tagion.crypto.SecureInterfaceNet;
15 import tagion.crypto.SecureNet : StdSecureNet;
16 import tagion.dart.DARTcrud;
17 import tagion.hashgraph.Refinement;
18 import tagion.hibon.Document;
19 import tagion.hibon.Document;
20 import tagion.hibon.HiBONJSON;
21 import tagion.hibon.HiBONRecord;
22 import tagion.logger.LogRecords : LogInfo;
23 import tagion.logger.Logger;
24 import tagion.script.Currency : totalAmount;
25 import tagion.script.TagionCurrency;
26 import tagion.script.common;
27 import tagion.script.execute;
28 import tagion.services.options;
29 import tagion.testbench.actor.util;
30 import tagion.testbench.services.helper_functions;
31 import tagion.testbench.tools.Environment;
32 import tagion.tools.wallet.WalletInterface;
33 import tagion.utils.pretend_safe_concurrency : receiveOnly, receiveTimeout, register;
34 import tagion.wallet.SecureWallet : SecureWallet;
35 
36 alias StdSecureWallet = SecureWallet!StdSecureNet;
37 enum CONTRACT_TIMEOUT = 40;
38 enum EPOCH_TIMEOUT = 15;
39 
40 enum feature = Feature(
41             "double spend scenarios",
42             []);
43 
44 alias FeatureContext = Tuple!(
45         SameInputsSpendOnOneContract, "SameInputsSpendOnOneContract",
46         OneContractWhereSomeBillsAreUsedTwice, "OneContractWhereSomeBillsAreUsedTwice",
47         DifferentContractsDifferentNodes, "DifferentContractsDifferentNodes",
48         SameContractDifferentNodes, "SameContractDifferentNodes",
49         SameContractInDifferentEpochs, "SameContractInDifferentEpochs",
50         SameContractInDifferentEpochsDifferentNode, "SameContractInDifferentEpochsDifferentNode",
51         TwoContractsSameOutput, "TwoContractsSameOutput",
52         BillAge, "BillAge",
53         FeatureGroup*, "result"
54 );
55 
56 @safe @Scenario("Same inputs spend on one contract",
57         [])
58 class SameInputsSpendOnOneContract {
59 
60     Options opts;
61     StdSecureWallet wallet1;
62     StdSecureWallet wallet2;
63     //
64     SignedContract signed_contract;
65 
66     this(Options opts, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
67         this.wallet1 = wallet1;
68         this.wallet2 = wallet2;
69         this.opts = opts;
70     }
71     import tagion.services.collector : reject_collector;
72 
73     @Given("i have a malformed contract correctly signed with two inputs which are the same")
74     Document same() {
75         const amount_to_pay = 1100.TGN;
76         auto payment_request = wallet2.requestBill(amount_to_pay);
77 
78         auto wallet1_bill = wallet1.account.bills.front;
79         check(wallet1_bill.value == 1000.TGN, "should be 1000 tgn");
80 
81         PayScript pay_script;
82         pay_script.outputs = [payment_request];
83         
84         TagionBill[] collected_bills = [wallet1_bill, wallet1_bill];
85         const fees = ContractExecution.billFees(collected_bills.map!(b=> b.toDoc), pay_script.outputs.map!(b => b.toDoc), 20);
86 
87         const total_collected_amount = collected_bills
88             .map!(bill => bill.value)
89             .totalAmount;
90 
91         const amount_remainder = total_collected_amount - amount_to_pay - fees;
92         const nets = wallet1.collectNets(collected_bills);
93         const bill_remain = wallet1.requestBill(amount_remainder);
94         pay_script.outputs ~= bill_remain;
95         wallet1.lock_bills(collected_bills);
96         
97         check(nets.length == collected_bills.length, format("number of bills does not match number of signatures nets %s, collected_bills %s", nets
98                     .length, collected_bills.length));
99         
100         signed_contract = sign(
101             nets,
102             collected_bills.map!(bill => bill.toDoc)
103             .array,
104             null,
105             pay_script.toDoc
106         );
107 
108         check(signed_contract.contract.inputs.length == 2, "should contain two inputs");
109         return result_ok;
110     }
111 
112     @When("i send the contract to the network")
113     Document network() {
114 
115         submask.subscribe(reject_collector);
116         auto wallet1_hirpc = HiRPC(wallet1.net);
117         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
118         writefln("---SUBMIT ADDRESS--- %s", opts.inputvalidator.sock_addr); 
119         sendSubmitHiRPC(opts.inputvalidator.sock_addr, hirpc_submit, wallet1.net);
120 
121         return result_ok;
122     }
123     @Then("the contract should be rejected.")
124     Document dart() {
125         auto result = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
126         check(result[0].symbol_name == "missing_archives", format("did not reject for the expected reason %s", result[0].symbol_name));
127         submask.unsubscribe(reject_collector);
128         return result_ok;
129     }
130 
131 }
132 
133 @safe @Scenario("one contract where some bills are used twice.",
134         [])
135 class OneContractWhereSomeBillsAreUsedTwice {
136     Options opts;
137     StdSecureWallet wallet1;
138     StdSecureWallet wallet2;
139     //
140     SignedContract signed_contract;
141 
142     this(Options opts, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
143         this.wallet1 = wallet1;
144         this.wallet2 = wallet2;
145         this.opts = opts;
146     }
147 
148     import tagion.services.collector : reject_collector;
149     @Given("i have a malformed contract correctly signed with three inputs where to are the same.")
150     Document same() {
151         const amount_to_pay = 2500.TGN;
152         auto payment_request = wallet2.requestBill(amount_to_pay);
153 
154         auto wallet1_bill = wallet1.account.bills[0];
155         auto wallet2_bill = wallet1.account.bills[1];
156         check(wallet1_bill.value == 1000.TGN, "should be 1000 tgn");
157         check(wallet2_bill.value == 1000.TGN, "should be 1000 tgn");
158 
159         PayScript pay_script;
160         pay_script.outputs = [payment_request];
161         
162         TagionBill[] collected_bills = [wallet1_bill, wallet1_bill, wallet2_bill];
163         const fees = ContractExecution.billFees(collected_bills.map!(b=> b.toDoc), pay_script.outputs.map!(b=> b.toDoc),100);
164 
165         const total_collected_amount = collected_bills
166             .map!(bill => bill.value)
167             .totalAmount;
168 
169         const amount_remainder = total_collected_amount - amount_to_pay - fees;
170         const nets = wallet1.collectNets(collected_bills);
171         const bill_remain = wallet1.requestBill(amount_remainder);
172         pay_script.outputs ~= bill_remain;
173         wallet1.lock_bills(collected_bills);
174         
175         check(nets.length == collected_bills.length, format("number of bills does not match number of signatures nets %s, collected_bills %s", nets
176                     .length, collected_bills.length));
177         
178         signed_contract = sign(
179             nets,
180             collected_bills.map!(bill => bill.toDoc)
181             .array,
182             null,
183             pay_script.toDoc
184         );
185 
186         check(signed_contract.contract.inputs.length == 3, "should contain two inputs");
187         check(signed_contract.contract.inputs.uniq.array.length == 2, "should be malformed and contain two identical and one different bill");
188         return result_ok;
189     }
190 
191     @When("i send the contract to the network")
192     Document network() {
193         submask.subscribe(reject_collector);
194         auto wallet1_hirpc = HiRPC(wallet1.net);
195         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
196         writefln("---SUBMIT ADDRESS--- %s", opts.inputvalidator.sock_addr); 
197         sendSubmitHiRPC(opts.inputvalidator.sock_addr, hirpc_submit, wallet1.net);
198 
199         return result_ok;
200     }
201     @Then("the contract should be rejected.")
202     Document dart() {
203         auto result = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
204         check(result[0].symbol_name== "missing_archives", format("did not reject for the expected reason %s", result[0].symbol_name));
205         submask.unsubscribe(reject_collector);
206         return result_ok;
207     }
208 
209 }
210 
211 @safe @Scenario("Different contracts different nodes.",
212         [])
213 class DifferentContractsDifferentNodes {
214     Options opts1;
215     Options opts2;
216     StdSecureWallet wallet1;
217     StdSecureWallet wallet2;
218     //
219     SignedContract signed_contract1;
220     SignedContract signed_contract2;
221     TagionCurrency amount;
222     TagionCurrency fee;
223 
224     HiRPC wallet1_hirpc;
225     HiRPC wallet2_hirpc;
226     TagionCurrency start_amount1;
227     TagionCurrency start_amount2;
228 
229     this(Options opts1, Options opts2, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
230         this.wallet1 = wallet1;
231         this.wallet2 = wallet2;
232         this.opts1 = opts1;
233         this.opts2 = opts2;
234 
235         wallet1_hirpc = HiRPC(wallet1.net);
236         wallet2_hirpc = HiRPC(wallet2.net);
237         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
238         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
239         
240     }
241     @Given("i have two correctly signed contracts.")
242     Document contracts() {
243 
244         amount = 100.TGN;
245         auto payment_request1 = wallet1.requestBill(amount);
246         auto payment_request2 = wallet2.requestBill(amount);
247 
248 
249         check(wallet1.createPayment([payment_request2], signed_contract1, fee).value, "Error creating payment wallet");
250 
251         check(wallet2.createPayment([payment_request1], signed_contract2, fee).value, "Error creating payment wallet");
252         return result_ok;
253     }
254 
255     @When("i send the contracts to the network at the same time.")
256     Document time() {
257         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract1), wallet1.net);
258         sendSubmitHiRPC(opts2.inputvalidator.sock_addr, wallet2_hirpc.submit(signed_contract2), wallet2.net);
259         return result_ok;
260     }
261 
262     @Then("both contracts should go through.")
263     Document through() {
264         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
265 
266 
267         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
268         writefln("WALLET 1 amount: %s", wallet1_amount);
269         check(wallet1_amount == start_amount1 - fee, "did not receive tx");
270         
271         auto wallet2_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet2_hirpc);
272         writefln("WALLET 2 amount: %s", wallet2_amount);
273         check(wallet2_amount == start_amount2 - fee, "did not receive tx");
274         return result_ok;
275     }
276 }
277 
278 
279 @safe @Scenario("Same contract different nodes.",
280         [])
281 class SameContractDifferentNodes {
282     Options opts1;
283     Options opts2;
284     StdSecureWallet wallet1;
285     StdSecureWallet wallet2;
286     //
287     SignedContract signed_contract;
288     TagionCurrency amount;
289     TagionCurrency fee;
290 
291     HiRPC wallet1_hirpc;
292     HiRPC wallet2_hirpc;
293     TagionCurrency start_amount1;
294     TagionCurrency start_amount2;
295 
296     this(Options opts1, Options opts2, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
297         this.wallet1 = wallet1;
298         this.wallet2 = wallet2;
299         this.opts1 = opts1;
300         this.opts2 = opts2;
301         wallet1_hirpc = HiRPC(wallet1.net);
302         wallet2_hirpc = HiRPC(wallet2.net);
303         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
304         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
305     }
306 
307     @Given("i have a correctly signed contract.")
308     Document contract() {
309 
310         
311         writefln("SAME CONTRACT DIFFERENT NODES");
312         amount = 1500.TGN;
313         auto payment_request = wallet2.requestBill(amount);
314         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating wallet");
315         check(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, "signed contract inputs invalid");
316 
317         return result_ok;
318     }
319 
320     @When("i send the same contract to two different nodes.")
321     Document nodes() {
322         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
323         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, hirpc_submit,wallet1.net);
324         sendSubmitHiRPC(opts2.inputvalidator.sock_addr, hirpc_submit,wallet1.net);
325 
326         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
327         return result_ok;
328     }
329 
330     @Then("the first contract should go through and the second one should be rejected.")
331     Document rejected() {
332         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
333         writefln("WALLET 1 amount: %s", wallet1_amount);
334         const wallet1_expected = start_amount1-amount-fee;
335         check(wallet1_amount == wallet1_expected, format("wallet 1 did not lose correct amount of money, should have %s, had %s", wallet1_expected, wallet1_amount));
336 
337         auto wallet2_amount = getWalletUpdateAmount(wallet2, opts1.dart_interface.sock_addr, wallet2_hirpc);
338         writefln("WALLET 2 amount: %s", wallet2_amount);
339         check(wallet2_amount == start_amount2+amount, "did not receive money");
340         return result_ok;
341 
342 
343         
344         const wallet2_expected = start_amount2+amount;
345         check(wallet2_amount == wallet2_expected, format("wallet 2 did not lose correct amount of money, should have %s, had %s", wallet2_expected, wallet2_amount));
346     }
347 
348 }
349 
350 @safe @Scenario("Same contract in different epochs.",
351         [])
352 class SameContractInDifferentEpochs {
353 
354     Options opts1;
355     StdSecureWallet wallet1;
356     StdSecureWallet wallet2;
357     //
358     SignedContract signed_contract;
359     TagionCurrency amount;
360     TagionCurrency fee;
361 
362     HiRPC wallet1_hirpc;
363     HiRPC wallet2_hirpc;
364     TagionCurrency start_amount1;
365     TagionCurrency start_amount2;
366 
367     this(Options opts1, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
368         this.wallet1 = wallet1;
369         this.wallet2 = wallet2;
370         this.opts1 = opts1;
371         wallet1_hirpc = HiRPC(wallet1.net);
372         wallet2_hirpc = HiRPC(wallet2.net);
373         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
374         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
375     }
376     @Given("i have a correctly signed contract.")
377     Document contract() {
378         submask.subscribe(StdRefinement.epoch_created);
379 
380         writefln("SAME CONTRACT different epoch");
381         amount = 1500.TGN;
382         auto payment_request = wallet2.requestBill(amount);
383         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating wallet");
384         check(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, "signed contract inputs invalid");
385 
386         return result_ok;
387     }
388 
389     @When("i send the contract to the network in different epochs to the same node.")
390     Document node() {
391         import tagion.hashgraph.Refinement : FinishedEpoch;
392 
393         long epoch_number;
394         uint max_tries = 20;
395         uint counter;
396         do {
397             auto epoch_before = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
398             writefln("epoch_before %s looking for %s", epoch_before[1], opts1.task_names.epoch_creator);
399             check(epoch_before[1].isRecord!FinishedEpoch, "not correct subscription received");
400             if (epoch_before[0].task_name == opts1.task_names.epoch_creator) {
401                 writefln("################### CAME IN ################");
402                 epoch_number = FinishedEpoch(epoch_before[1]).epoch;
403             }
404             counter++;
405         } while(counter < max_tries && epoch_number is long.init);
406         check(counter < max_tries, "did not receive epoch in max tries");
407 
408         writefln("EPOCH NUMBER %s", epoch_number);
409 
410         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
411         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, hirpc_submit,wallet1.net);
412 
413         long new_epoch_number;
414         counter = 0;
415         do {
416             auto new_epoch = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
417             writefln("new_epoch %s %s", new_epoch[0].topic_name, opts1.task_names.epoch_creator);
418             check(new_epoch[1].isRecord!FinishedEpoch, "not correct subscription received");
419             if (new_epoch[0].task_name == opts1.task_names.epoch_creator) {
420                 writefln("UPDATING NEW EPOCH_NUMBER");
421                 new_epoch_number = FinishedEpoch(new_epoch[1]).epoch;
422             }
423             counter++;
424         } while(counter < max_tries && new_epoch_number is long.init);
425         check(counter < max_tries, "did not receive epoch in max tries");
426 
427         submask.unsubscribe(StdRefinement.epoch_created);
428         writefln("EPOCH NUMBER updated %s", new_epoch_number);
429         check(epoch_number < new_epoch_number, "epoch number not updated");
430         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, hirpc_submit, wallet1.net);
431         
432         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
433         return result_ok;
434     }
435 
436     @Then("the first contract should go through and the second one should be rejected.")
437     Document rejected() {
438         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
439         auto wallet2_amount = getWalletUpdateAmount(wallet2, opts1.dart_interface.sock_addr, wallet2_hirpc);
440         writefln("WALLET 1 amount: %s", wallet1_amount);
441         writefln("WALLET 2 amount: %s", wallet2_amount);
442 
443         const expected_amount1 = start_amount1-amount-fee;
444         const expected_amount2 = start_amount2 + amount;
445         check(wallet1_amount == expected_amount1, format("wallet 1 did not lose correct amount of money should have %s had %s", expected_amount1, wallet1_amount));
446         check(wallet2_amount == expected_amount2, format("wallet 2 did not lose correct amount of money should have %s had %s", expected_amount2, wallet2_amount));
447 
448 
449         return result_ok;
450     }
451 
452 }
453 
454 @safe @Scenario("Same contract in different epochs different node.",
455         [])
456 class SameContractInDifferentEpochsDifferentNode {
457     Options opts1;
458     Options opts2;
459     StdSecureWallet wallet1;
460     StdSecureWallet wallet2;
461     //
462     SignedContract signed_contract;
463     TagionCurrency amount;
464     TagionCurrency fee;
465 
466     HiRPC wallet1_hirpc;
467     HiRPC wallet2_hirpc;
468     TagionCurrency start_amount1;
469     TagionCurrency start_amount2;
470 
471     this(Options opts1,Options opts2, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
472         this.wallet1 = wallet1;
473         this.wallet2 = wallet2;
474         this.opts1 = opts1;
475         this.opts2 = opts2;
476         wallet1_hirpc = HiRPC(wallet1.net);
477         wallet2_hirpc = HiRPC(wallet2.net);
478         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
479         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
480     }
481 
482     @Given("i have a correctly signed contract.")
483     Document contract() {
484         submask.subscribe(StdRefinement.epoch_created);
485 
486         writefln("SAME CONTRACT different node different epoch");
487         amount = 1500.TGN;
488         auto payment_request = wallet2.requestBill(amount);
489         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating payment");
490         check(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, "signed contract inputs invalid");
491 
492         return result_ok;
493     }
494 
495     @When("i send the contract to the network in different epochs to different nodes.")
496     Document nodes() {
497         import tagion.hashgraph.Refinement : FinishedEpoch;
498         uint max_tries = 20;
499         uint counter;
500 
501         long epoch_number;
502         do {
503             auto epoch_before = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
504             writefln("epoch_before %s looking for %s", epoch_before[1], opts1.task_names.epoch_creator);
505             check(epoch_before[1].isRecord!FinishedEpoch, "not correct subscription received");
506             if (epoch_before[0].task_name == opts1.task_names.epoch_creator) {
507                 epoch_number = FinishedEpoch(epoch_before[1]).epoch;
508             }
509             counter++;
510         } while(counter < max_tries && epoch_number is long.init);
511         check(counter < max_tries, "did not receive epoch in max tries");
512 
513         writeln("EPOCH NUMBER %s", epoch_number);
514 
515         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
516         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, hirpc_submit, wallet1.net);
517 
518         long new_epoch_number;
519         counter = 0;
520         do {
521             auto new_epoch = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
522             writefln("new_epoch %s %s", new_epoch[1], opts1.task_names.epoch_creator);
523             check(new_epoch[1].isRecord!FinishedEpoch, "not correct subscription received");
524             if (new_epoch[0].task_name == opts2.task_names.epoch_creator) {
525                 writefln("UPDATING NEW EPOCH_NUMBER");
526                 long _new_epoch_number = FinishedEpoch(new_epoch[1]).epoch;
527                 if (_new_epoch_number > epoch_number) {
528                     new_epoch_number = _new_epoch_number;
529                 }
530             }
531             counter++;
532         } while(counter < max_tries && new_epoch_number is long.init);
533         check(counter < max_tries, "did not receive epoch in max tries");
534 
535         writeln("EPOCH NUMBER updated %s", new_epoch_number);
536         sendSubmitHiRPC(opts2.inputvalidator.sock_addr, hirpc_submit, wallet1.net);
537         
538         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
539         return result_ok;
540     }
541 
542     @Then("the first contract should go through and the second one should be rejected.")
543     Document rejected() {
544         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
545         auto wallet2_amount = getWalletUpdateAmount(wallet2, opts1.dart_interface.sock_addr, wallet2_hirpc);
546         writefln("WALLET 1 amount: %s", wallet1_amount);
547         writefln("WALLET 2 amount: %s", wallet2_amount);
548 
549         const expected_amount1 = start_amount1-amount-fee;
550         const expected_amount2 = start_amount2 + amount;
551         check(wallet1_amount == expected_amount1, format("wallet 1 did not lose correct amount of money should have %s had %s", expected_amount1, wallet1_amount));
552         check(wallet2_amount == expected_amount2, format("wallet 2 did not lose correct amount of money should have %s had %s", expected_amount2, wallet2_amount));
553 
554         submask.unsubscribe(StdRefinement.epoch_created);
555 
556         return result_ok;
557     }
558 
559 }
560 
561 @safe @Scenario("Two contracts same output",
562         [])
563 class TwoContractsSameOutput {
564     Options opts1;
565     Options opts2;
566     StdSecureWallet wallet1;
567     StdSecureWallet wallet2;
568     StdSecureWallet wallet3;
569     //
570     SignedContract signed_contract1;
571     SignedContract signed_contract2;
572     TagionCurrency amount;
573     TagionCurrency fee;
574 
575     HiRPC wallet1_hirpc;
576     HiRPC wallet2_hirpc;
577     HiRPC wallet3_hirpc;
578     TagionCurrency start_amount1;
579     TagionCurrency start_amount2;
580     TagionCurrency start_amount3;
581 
582     this(Options opts1,Options opts2, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2, ref StdSecureWallet wallet3) {
583         this.wallet1 = wallet1;
584         this.wallet2 = wallet2;
585         this.wallet3 = wallet3;
586         this.opts1 = opts1;
587         this.opts2 = opts2;
588         wallet1_hirpc = HiRPC(wallet1.net);
589         wallet2_hirpc = HiRPC(wallet2.net);
590         wallet3_hirpc = HiRPC(wallet3.net);
591         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
592         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
593         start_amount3 = wallet3.calcTotal(wallet3.account.bills);
594     }
595 
596     @Given("i have a payment request containing a bill.")
597     Document bill() {
598         amount = 333.TGN;
599         auto payment_request = wallet3.requestBill(amount);
600         check(wallet1.createPayment([payment_request], signed_contract1, fee).value, "Error paying wallet");
601         check(wallet2.createPayment([payment_request], signed_contract2, fee).value, "Error paying wallet");
602 
603         check(signed_contract1.contract.inputs.uniq.array.length == signed_contract1.contract.inputs.length, "signed contract inputs invalid");
604         check(signed_contract2.contract.inputs.uniq.array.length == signed_contract2.contract.inputs.length, "signed contract inputs invalid");
605 
606         return result_ok;
607     }
608 
609     @When("i pay the bill from two different wallets.")
610     Document wallets() {
611         auto hirpc_submit1 = wallet1_hirpc.submit(signed_contract1);
612         auto hirpc_submit2 = wallet2_hirpc.submit(signed_contract2);
613         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, hirpc_submit1, wallet1.net);
614         sendSubmitHiRPC(opts2.inputvalidator.sock_addr, hirpc_submit2, wallet2.net);
615 
616 
617         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
618         return result_ok;
619     }
620 
621     @Then("only one output should be produced.")
622     Document produced() {
623         
624         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
625         writefln("WALLET 1 amount: %s", wallet1_amount);
626         const expected = start_amount1-amount-fee;
627         check(wallet1_amount == expected, format("wallet 1 did not lose correct amount of money should have %s had %s", expected, wallet1_amount));
628 
629         auto wallet2_amount = getWalletUpdateAmount(wallet2, opts2.dart_interface.sock_addr, wallet2_hirpc);
630         writefln("WALLET 2 amount: %s", wallet2_amount);
631         check(wallet2_amount == start_amount2-amount-fee, "wallet 2 did not lose correct amount of money");
632 
633         auto wallet3_amount = getWalletUpdateAmount(wallet3, opts1.dart_interface.sock_addr, wallet3_hirpc);
634         writefln("WALLET 3 amount: %s", wallet3_amount);
635         check(wallet3_amount == start_amount3+amount, format("did not receive money correct amount of money should have %s had %s", start_amount3+amount, wallet3_amount));
636         return result_ok;
637     }
638 
639 }
640 
641 @safe @Scenario("Bill age",
642         [])
643 class BillAge {
644     Options opts1;
645     StdSecureWallet wallet1;
646     StdSecureWallet wallet2;
647     //
648     SignedContract signed_contract;
649     TagionCurrency amount;
650     TagionCurrency fee;
651 
652     HiRPC wallet1_hirpc;
653     HiRPC wallet2_hirpc;
654     TagionCurrency start_amount1;
655     TagionCurrency start_amount2;
656 
657     this(Options opts1, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
658         this.wallet1 = wallet1;
659         this.wallet2 = wallet2;
660         this.opts1 = opts1;
661 
662         wallet1_hirpc = HiRPC(wallet1.net);
663         wallet2_hirpc = HiRPC(wallet2.net);
664         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
665         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
666         
667     }
668 
669     @Given("i pay a contract where the output bills timestamp is newer than epoch_time + constant.")
670     Document constant() {
671 
672         import std.datetime;
673         import tagion.services.transcript : BUFFER_TIME_SECONDS;
674         import tagion.utils.StdTime;
675 
676         amount = 100.TGN;
677         auto new_time = sdt_t((SysTime(cast(long) currentTime) + BUFFER_TIME_SECONDS.seconds + 100.seconds).stdTime);
678 
679         auto payment_request = wallet2.requestBill(amount, new_time);
680 
681         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating payment");
682         check(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, "signed contract inputs invalid");
683 
684         return result_ok;
685     }
686 
687     @When("i send the contract to the network.")
688     Document network() {
689         sendSubmitHiRPC(opts1.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract), wallet1.net);
690         return result_ok;
691     }
692 
693     @Then("the contract should be rejected.")
694     Document rejected() {
695         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
696 
697         auto wallet1_amount = getWalletUpdateAmount(wallet1, opts1.dart_interface.sock_addr, wallet1_hirpc);
698         auto wallet1_total_amount = wallet1.account.total;
699         writefln("WALLET 1 TOTAL amount: %s", wallet1_total_amount);
700         check(wallet1_total_amount == start_amount1, format("wallet total amount not correct. expected: %s, had %s", start_amount1, wallet1_total_amount));
701 
702         auto wallet2_amount = getWalletUpdateAmount(wallet2, opts1.dart_interface.sock_addr, wallet2_hirpc);
703         writefln("WALLET 2 amount: %s", wallet2_amount);
704         check(wallet2_amount == start_amount2, "should not receive new money");
705         
706         return result_ok;
707     }
708 
709 }