1 module tagion.testbench.services.spam_double_spend;
2 // Default import list for bdd
3 import std.typecons : Tuple;
4 import tagion.actor;
5 import tagion.behaviour;
6 import tagion.communication.HiRPC;
7 import tagion.crypto.SecureInterfaceNet;
8 import tagion.crypto.SecureNet : StdSecureNet;
9 import tagion.dart.DARTcrud;
10 import tagion.hibon.Document;
11 import tagion.hibon.Document;
12 import tagion.hibon.HiBONJSON;
13 import tagion.hibon.HiBONRecord;
14 import tagion.logger.LogRecords : LogInfo;
15 import tagion.logger.Logger;
16 import tagion.script.Currency : totalAmount;
17 import tagion.script.TagionCurrency;
18 import tagion.script.common;
19 import tagion.script.execute;
20 import tagion.services.options;
21 import tagion.testbench.actor.util;
22 import tagion.testbench.services.helper_functions;
23 import tagion.testbench.tools.Environment;
24 import tagion.tools.wallet.WalletInterface;
25 import tagion.utils.pretend_safe_concurrency : receiveOnly, receiveTimeout;
26 import tagion.wallet.SecureWallet : SecureWallet;
27 
28 import core.thread;
29 import core.time;
30 import std.algorithm;
31 import std.format;
32 import std.range;
33 import std.stdio;
34 
35 alias StdSecureWallet = SecureWallet!StdSecureNet;
36 enum CONTRACT_TIMEOUT = 40;
37 enum EPOCH_TIMEOUT = 15;
38 
39 enum feature = Feature(
40             "Spam the network with the same contracts until we know it does not go through.",
41             []);
42 
43 alias FeatureContext = Tuple!(
44         SpamOneNodeUntil10EpochsHaveOccured, "SpamOneNodeUntil10EpochsHaveOccured",
45         SpamMultipleNodesUntil10EpochsHaveOccured, "SpamMultipleNodesUntil10EpochsHaveOccured",
46         FeatureGroup*, "result"
47 );
48 
49 @safe @Scenario("Spam one node until 10 epochs have occured.",
50         [])
51 class SpamOneNodeUntil10EpochsHaveOccured {
52 
53     Options node1_opts;
54     Options[] opts;
55     StdSecureWallet wallet1;
56     StdSecureWallet wallet2;
57     TagionCurrency amount;
58     TagionCurrency fee;
59     //
60     SignedContract signed_contract;
61     HiRPC wallet1_hirpc;
62     HiRPC wallet2_hirpc;
63     TagionCurrency start_amount1;
64     TagionCurrency start_amount2;
65 
66     this(Options[] opts, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
67         this.wallet1 = wallet1;
68         this.wallet2 = wallet2;
69         this.node1_opts = opts[0];
70         this.opts = opts;
71         wallet1_hirpc = HiRPC(wallet1.net);
72         wallet2_hirpc = HiRPC(wallet2.net);
73         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
74         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
75     }
76 
77     @Given("i have a correctly signed contract.")
78     Document contract() {
79         amount = 100.TGN;
80         auto payment_request = wallet2.requestBill(amount);
81         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating payment wallet");
82 
83         return result_ok;
84     }
85 
86     @When("i continue to send the same contract with n delay to one node.")
87     Document node() {
88         import tagion.hashgraph.Refinement : FinishedEpoch;
89 
90         thisActor.task_name = "spam_contract_task";
91         log.registerSubscriptionTask(thisActor.task_name);
92         submask.subscribe("epoch_creator/epoch_created");
93 
94         long epoch_number;
95 
96         auto epoch_before = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
97         check(epoch_before[1].isRecord!FinishedEpoch, "not correct subscription received");
98         epoch_number = FinishedEpoch(epoch_before[1]).epoch;
99 
100         long current_epoch_number;
101 
102         while (current_epoch_number < epoch_number + 10) {
103             sendSubmitHiRPC(node1_opts.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract), wallet1.net);
104             (() @trusted => Thread.sleep(100.msecs))();
105 
106             auto current_epoch = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
107             check(current_epoch[1].isRecord!FinishedEpoch, "not correct subscription received");
108             current_epoch_number = FinishedEpoch(current_epoch[1]).epoch;
109             writefln("epoch_number %s, CURRENT EPOCH %s", epoch_number, current_epoch_number);
110         }
111 
112         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
113         return result_ok;
114     }
115 
116     @Then("only the first contract should go through and the other ones should be rejected.")
117     Document rejected() {
118         auto wallet1_amount = getWalletUpdateAmount(wallet1, node1_opts.dart_interface.sock_addr, wallet1_hirpc);
119         auto wallet2_amount = getWalletUpdateAmount(wallet2, node1_opts.dart_interface.sock_addr, wallet2_hirpc);
120         writefln("WALLET 1 amount: %s", wallet1_amount);
121         writefln("WALLET 2 amount: %s", wallet2_amount);
122 
123         const expected_amount1 = start_amount1 - amount - fee;
124         const expected_amount2 = start_amount2 + amount;
125         check(wallet1_amount == expected_amount1, format(
126                 "wallet 1 did not lose correct amount of money should have %s had %s", expected_amount1, wallet1_amount));
127         check(wallet2_amount == expected_amount2, format(
128                 "wallet 2 did not lose correct amount of money should have %s had %s", expected_amount2, wallet2_amount));
129 
130         return result_ok;
131     }
132 
133     // @Then("check that the bullseye is the same across all nodes.")
134     // Document nodes() {
135     //     import tagion.dart.DARTBasic;
136     //     import tagion.dart.DARTFile;
137     //     import tagion.Keywords;
138     //     import tagion.basic.Types;
139 
140     //     Buffer[] eyes;
141     //     foreach(opt; opts) {
142     //         auto bullseye_sender = dartBullseye();
143     //         auto hirpc_bullseye_receiver = sendDARTHiRPC(opt.dart_interface.sock_addr, bullseye_sender, wallet1_hirpc);
144     //         check(!hirpc_bullseye_receiver.isError, format("senddarthirpc received error: %s", hirpc_bullseye_receiver.toPretty));
145     //         auto hirpc_message = hirpc_bullseye_receiver.message[Keywords.result].get!Document;
146     //         auto bullseye = hirpc_message[DARTFile.Params.bullseye].get!Buffer;
147     //         eyes ~= bullseye;
148     //     }
149 
150     //     import tagion.hibon.HiBONtoText;
151 
152     //     writefln("%s", eyes);
153     //     foreach(eye; eyes) {
154     //         check(eye == eyes[0], "bullseyes not the same across nodes");
155     //     }
156 
157     //     return result_ok;
158     // }
159 
160 }
161 
162 import tagion.actor;
163 
164 @safe
165 struct SpamWorker {
166     import tagion.hashgraph.Refinement;
167 
168     void task(immutable(Options) opts, immutable(SecureNet) net, immutable(SignedContract) signed_contract) {
169 
170         HiRPC hirpc = HiRPC(net);
171 
172         setState(Ctrl.ALIVE);
173 
174         writefln("registrering subscription mask %s", thisActor.task_name);
175         log.registerSubscriptionTask(thisActor.task_name);
176         submask.subscribe(StdRefinement.epoch_created);
177         long epoch_number;
178 
179         while (!thisActor.stop && epoch_number is long.init) {
180             writefln("WAITING FOR RECEIVE");
181             auto epoch_before = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
182             writefln("AFTER RECEIVE %s", epoch_before);
183             if (epoch_before[0].task_name == opts.task_names.epoch_creator) {
184                 epoch_number = FinishedEpoch(epoch_before[1]).epoch;
185             }
186         }
187 
188         long current_epoch_number;
189         while (!thisActor.stop && current_epoch_number < epoch_number + 10) {
190             sendSubmitHiRPC(opts.inputvalidator.sock_addr, hirpc.submit(signed_contract), net);
191             (() @trusted => Thread.sleep(100.msecs))();
192 
193             auto current_epoch = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
194             if (current_epoch[0].task_name != opts.task_names.epoch_creator) {
195                 current_epoch_number = FinishedEpoch(current_epoch[1]).epoch;
196                 writefln("epoch_number %s, CURRENT EPOCH %s", epoch_number, current_epoch_number);
197             }
198         }
199         submask.unsubscribe(StdRefinement.epoch_created);
200         thisActor.stop = true;
201     }
202 
203 }
204 
205 @safe @Scenario("Spam multiple nodes until 10 epochs have occured.",
206         [])
207 class SpamMultipleNodesUntil10EpochsHaveOccured {
208     Options[] opts;
209     StdSecureWallet wallet1;
210     StdSecureWallet wallet2;
211     TagionCurrency amount;
212     TagionCurrency fee;
213     //
214     SignedContract signed_contract;
215     HiRPC wallet1_hirpc;
216     HiRPC wallet2_hirpc;
217     TagionCurrency start_amount1;
218     TagionCurrency start_amount2;
219 
220     this(Options[] opts, ref StdSecureWallet wallet1, ref StdSecureWallet wallet2) {
221         this.wallet1 = wallet1;
222         this.wallet2 = wallet2;
223         this.opts = opts;
224         wallet1_hirpc = HiRPC(wallet1.net);
225         wallet2_hirpc = HiRPC(wallet2.net);
226         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
227         start_amount2 = wallet2.calcTotal(wallet2.account.bills);
228     }
229 
230     @Given("i have a correctly signed contract.")
231     Document signedContract() {
232         writefln("######## NEXT TEST ########");
233         amount = 100.TGN;
234         auto payment_request = wallet2.requestBill(amount);
235         check(wallet1.createPayment([payment_request], signed_contract, fee).value, "Error creating payment wallet");
236 
237         return result_ok;
238     }
239 
240     @When("i continue to send the same contract with n delay to multiple nodes.")
241     Document multipleNodes() @trusted {
242         ActorHandle[] handles;
243 
244         foreach (i, opt; opts) {
245             handles ~= spawn!SpamWorker(format("spam_worker%s", i), cast(immutable) opt, cast(immutable) wallet1.net, cast(
246                     immutable) signed_contract);
247         }
248         writefln("waiting for alive");
249         waitforChildren(Ctrl.ALIVE, 5.seconds);
250         writefln("waiting for end");
251         waitforChildren(Ctrl.END);
252 
253         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
254         return result_ok;
255     }
256 
257     @Then("only the first contract should go through and the other ones should be rejected.")
258     Document beRejected() {
259         auto node1_opts = opts[1];
260 
261         auto wallet1_amount = getWalletUpdateAmount(wallet1, node1_opts.dart_interface.sock_addr, wallet1_hirpc);
262         auto wallet2_amount = getWalletUpdateAmount(wallet2, node1_opts.dart_interface.sock_addr, wallet2_hirpc);
263         writefln("WALLET 1 amount: %s", wallet1_amount);
264         writefln("WALLET 2 amount: %s", wallet2_amount);
265 
266         const expected_amount1 = start_amount1 - amount - fee;
267         const expected_amount2 = start_amount2 + amount;
268         check(wallet1_amount == expected_amount1, format(
269                 "wallet 1 did not lose correct amount of money should have %s had %s", expected_amount1, wallet1_amount));
270         check(wallet2_amount == expected_amount2, format(
271                 "wallet 2 did not lose correct amount of money should have %s had %s", expected_amount2, wallet2_amount));
272 
273         return result_ok;
274     }
275 
276     // @Then("check that the bullseye is the same across all nodes.")
277     // Document allNodes() {
278     //     import tagion.dart.DARTBasic;
279     //     import tagion.dart.DARTFile;
280     //     import tagion.Keywords;
281     //     import tagion.basic.Types;
282 
283     //     Buffer[] eyes;
284     //     foreach(opt; opts) {
285     //         auto bullseye_sender = dartBullseye();
286     //         auto hirpc_bullseye_receiver = sendDARTHiRPC(opt.dart_interface.sock_addr, bullseye_sender, wallet1_hirpc);
287     //         check(!hirpc_bullseye_receiver.isError, format("senddarthirpc received error: %s", hirpc_bullseye_receiver.toPretty));
288     //         auto hirpc_message = hirpc_bullseye_receiver.message[Keywords.result].get!Document;
289     //         auto bullseye = hirpc_message[DARTFile.Params.bullseye].get!Buffer;
290     //         eyes ~= bullseye;
291     //     }
292 
293     //     import tagion.hibon.HiBONtoText;
294 
295     //     writefln("%s", eyes);
296     //     foreach(eye; eyes) {
297     //         check(eye == eyes[0], "bullseyes not the same across nodes");
298     //     }
299 
300     //     return result_ok;
301     // }
302 
303 }