1 module tagion.testbench.services.malformed_contract;
2 // Default import list for bdd
3 import std.typecons : Tuple;
4 import tagion.actor;
5 import tagion.basic.Types;
6 import tagion.behaviour;
7 import tagion.communication.HiRPC;
8 import tagion.crypto.SecureInterfaceNet;
9 import tagion.crypto.SecureNet : StdSecureNet;
10 import tagion.crypto.Types;
11 import tagion.dart.DARTcrud;
12 import tagion.hibon.Document;
13 import tagion.hibon.Document;
14 import tagion.hibon.HiBONJSON;
15 import tagion.hibon.HiBONRecord;
16 import tagion.logger.LogRecords : LogInfo;
17 import tagion.logger.Logger;
18 import tagion.script.Currency : totalAmount;
19 import tagion.script.TagionCurrency;
20 import tagion.script.common;
21 import tagion.script.execute;
22 import tagion.services.options;
23 import tagion.testbench.actor.util;
24 import tagion.testbench.services.helper_functions;
25 import tagion.testbench.tools.Environment;
26 import tagion.tools.wallet.WalletInterface;
27 import tagion.utils.pretend_safe_concurrency : receiveOnly, receiveTimeout;
28 import tagion.wallet.SecureWallet : SecureWallet;
29 
30 
31 import core.thread;
32 import core.time;
33 import std.algorithm;
34 import std.format;
35 import std.range;
36 import std.stdio;
37 
38 alias StdSecureWallet = SecureWallet!StdSecureNet;
39 enum CONTRACT_TIMEOUT = 25;
40 
41 enum feature = Feature(
42             "malformed contracts",
43             []);
44 
45 alias FeatureContext = Tuple!(
46         ContractTypeWithoutCorrectInformation, "ContractTypeWithoutCorrectInformation",
47         InputsAreNotBillsInDart, "InputsAreNotBillsInDart",
48         NegativeAmountAndZeroAmountOnOutputBills, "NegativeAmountAndZeroAmountOnOutputBills",
49         ContractWhereInputIsSmallerThanOutput, "ContractWhereInputIsSmallerThanOutput",
50         FeatureGroup*, "result"
51 );
52 import tagion.hashgraph.Refinement;
53 
54 @safe @Scenario("contract type without correct information",
55         [])
56 class ContractTypeWithoutCorrectInformation {
57     Options node1_opts;
58     StdSecureWallet wallet1;
59     SignedContract signed_contract;
60     HiRPC wallet1_hirpc;
61     TagionCurrency start_amount1;
62     bool epoch_on_startup; 
63 
64     this(Options opts, ref StdSecureWallet wallet1) {
65         this.wallet1 = wallet1;
66         this.node1_opts = opts;
67         wallet1_hirpc = HiRPC(wallet1.net);
68         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
69     }
70 
71 
72     import tagion.basic.Types : Buffer;
73     import tagion.crypto.Types : Pubkey;
74     import tagion.hibon.HiBONRecord;
75     import tagion.script.TagionCurrency;
76     import tagion.script.standardnames;
77     import tagion.utils.StdTime;
78 
79     @recordType("TGN") struct MaliciousBill {
80         @label(StdNames.value) @optional @(filter.Initialized) TagionCurrency value; /// Tagion bill 
81         @label(StdNames.time) @optional @(filter.Initialized) sdt_t time;
82         @label(StdNames.owner) @optional @(filter.Initialized) Pubkey owner;
83         @label(StdNames.nonce) @optional Buffer nonce; // extra nonce 
84         mixin HiBONRecord!(
85             q{
86                 this(const(TagionCurrency) value,const sdt_t time, Pubkey owner, Buffer nonce) pure nothrow {
87                     this.value = value;
88                     this.time = time;
89                     this.owner = owner;
90                     this.nonce = nonce;
91                 }
92             });
93     }
94 
95     @recordType("pay")
96     struct MaliciousPayScript {
97         @label(StdNames.values) const(MaliciousBill)[] outputs;
98         mixin HiBONRecord!(
99             q{
100                 this(const(MaliciousBill)[] outputs) pure nothrow {
101                     this.outputs = outputs;
102                 }
103             });
104     }
105     
106     @Given("i have a malformed signed contract where the type is correct but the fields are wrong.")
107     Document wrong() {
108         submask.subscribe(StdRefinement.epoch_created);
109         writeln("waiting for epoch");
110         epoch_on_startup = receiveTimeout(20.seconds, (LogInfo _, const(Document) __) {});
111         submask.unsubscribe(StdRefinement.epoch_created);
112         check(epoch_on_startup, "No epoch on startup");
113 
114 
115         // the bill to pay
116         const malicious_bill = MaliciousBill(10.TGN,sdt_t.init, Pubkey([1,2,3,4]), null);
117         MaliciousPayScript pay_script;
118         pay_script.outputs = [malicious_bill];
119 
120         TagionBill[] collected_bills = [wallet1.account.bills.front];
121         const nets = wallet1.collectNets(collected_bills);
122         check(nets.all!(net => net !is net.init), "Missing deriver of some of the bills");
123 
124         signed_contract = sign(
125             nets,
126             collected_bills.map!(bill => bill.toDoc).array,
127             null,
128             pay_script.toDoc
129         );
130 
131 
132         writefln("signed_contract %s", signed_contract.toDoc.toPretty);
133         
134         return result_ok;
135     }
136 
137     @When("i send the contract to the network.")
138     Document network() {
139         check(epoch_on_startup, "No epoch on startup");
140         submask.subscribe("error/tvm");
141 
142         sendSubmitHiRPC(node1_opts.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract), wallet1.net);
143         return result_ok;
144     }
145 
146     @Then("the contract should be rejected.")
147     Document rejected() {
148         check(epoch_on_startup, "No epoch on startup");
149         auto error = receiveOnlyTimeout!(LogInfo, const(Document))(CONTRACT_TIMEOUT.seconds);
150         submask.unsubscribe("error/tvm");
151         return result_ok;
152     }
153 
154 }
155 
156 @safe @Scenario("inputs are not bills in dart",
157         [])
158 class InputsAreNotBillsInDart {
159 
160     Options node1_opts;
161     StdSecureWallet wallet1;
162     SignedContract signed_contract;
163     HiRPC wallet1_hirpc;
164     TagionCurrency start_amount1;
165     const(Document) random_data;
166     bool epoch_on_startup;
167 
168     this(Options opts, ref StdSecureWallet wallet1, const(Document) random_data, bool epoch_on_startup) {
169         this.wallet1 = wallet1;
170         this.node1_opts = opts;
171         wallet1_hirpc = HiRPC(wallet1.net);
172         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
173         this.random_data = random_data;
174         this.epoch_on_startup = epoch_on_startup;
175     }
176     
177 
178     @Given("i have a malformed contract where the inputs are another type than bills.")
179     Document bills() {
180         check(epoch_on_startup, "No epoch on startup");
181         import tagion.script.common;
182 
183 
184         const bill = wallet1.requestBill(100.TGN);
185         PayScript pay_script;
186         pay_script.outputs = [bill];
187 
188         signed_contract = sign(
189             [wallet1.net],
190             [random_data],
191             null,
192             pay_script.toDoc
193         );
194 
195         writefln("NOTBILL signed_contract %s", signed_contract.toDoc.toPretty);
196 
197         return result_ok;
198             
199     }
200 
201     @When("i send the contract to the network.")
202     Document network() {
203         check(epoch_on_startup, "No epoch on startup");
204         submask.subscribe("error/tvm");
205         sendSubmitHiRPC(node1_opts.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract), wallet1.net);
206         return result_ok;
207     }
208 
209     @Then("the contract should be rejected.")
210     Document rejected() {
211         check(epoch_on_startup, "No epoch on startup");
212         auto error = receiveOnlyTimeout!(LogInfo, const(Document))(CONTRACT_TIMEOUT.seconds);
213         submask.unsubscribe("error/tvm");
214         return result_ok;
215     }
216 
217 }
218 
219 @safe @Scenario("Negative amount and zero amount on output bills.",
220         [])
221 class NegativeAmountAndZeroAmountOnOutputBills {
222     Options node1_opts;
223     StdSecureWallet wallet1;
224     SignedContract zero_contract;
225     SignedContract negative_contract;
226     SignedContract combined_contract;
227     HiRPC wallet1_hirpc;
228     TagionCurrency start_amount1;
229 
230     TagionBill[] used_bills;
231     TagionBill[] output_bills;
232     bool epoch_on_startup;
233     
234     this(Options opts, ref StdSecureWallet wallet1, bool epoch_on_startup) {
235         this.wallet1 = wallet1;
236         this.node1_opts = opts;
237         wallet1_hirpc = HiRPC(wallet1.net);
238         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
239         this.epoch_on_startup = epoch_on_startup;
240     }
241 
242     @Given("i have three contracts. One with output that is zero. Another where it is negative. And one with a negative and a valid output.")
243     Document output() {
244         check(epoch_on_startup, "No epoch on startup");
245         import tagion.hibon.HiBONtoText;
246         import tagion.script.common;
247         import tagion.utils.StdTime;
248 
249 
250 
251         const zero_bill = TagionBill(0.TGN,currentTime, Pubkey([1,2,3,4]), null);
252         const negative_bill = TagionBill(-1000.TGN, currentTime, Pubkey([4,3,2,1]), null);
253 
254         writefln("zero_bill = %s", zero_bill.toDoc.encodeBase64);
255         writefln("negative_bill = %s", negative_bill.toDoc.encodeBase64);
256 
257         PayScript zero_script;
258         PayScript negative_script;
259         PayScript combined_script;
260         negative_script.outputs = [negative_bill];
261         zero_script.outputs = [zero_bill];
262         combined_script.outputs = [zero_bill, negative_bill];
263 
264         output_bills = [zero_bill, negative_bill];
265 
266 
267         TagionBill[] input_bills1 = [wallet1.account.bills[0]];
268         TagionBill[] input_bills2 = [wallet1.account.bills[1]];
269         TagionBill[] input_bills3 = [wallet1.account.bills[2]];
270 
271         
272         used_bills = input_bills1 ~ input_bills2 ~ input_bills3;
273         wallet1.lock_bills(used_bills);
274 
275         const nets1 = wallet1.collectNets(input_bills1);
276         check(nets1.all!(net => net !is net.init), "Missing deriver of some of the bills");
277         zero_contract = sign(
278             nets1,
279             input_bills1.map!(bill => bill.toDoc).array,
280             null,
281             zero_script.toDoc
282         );
283         const nets2 = wallet1.collectNets(input_bills2);
284         check(nets2.all!(net => net !is net.init), "Missing deriver of some of the bills");
285         negative_contract = sign(
286             nets2,
287             input_bills2.map!(bill => bill.toDoc).array,
288             null,
289             negative_script.toDoc
290         );
291         const nets3 = wallet1.collectNets(input_bills3);
292         check(nets3.all!(net => net !is net.init), "Missing deriver of some of the bills");
293         combined_contract = sign(
294             nets3,
295             input_bills3.map!(bill => bill.toDoc).array,
296             null,
297             combined_script.toDoc
298         );
299 
300         writefln("zero_contract %s \n negative contract %s \n combined contract %s", zero_contract.toPretty, negative_contract.toPretty, combined_contract.toPretty);
301         
302         return result_ok;
303     }
304 
305     @When("i send the contracts to the network.")
306     Document network() {
307         check(epoch_on_startup, "No epoch on startup");
308         submask.subscribe("error/tvm");
309         foreach(contract; [zero_contract, negative_contract, combined_contract]) {
310             sendSubmitHiRPC(node1_opts.inputvalidator.sock_addr, wallet1_hirpc.submit(contract), wallet1.net);
311 
312             // auto error = receiveOnlyTimeout!(LogInfo, const(Document))(CONTRACT_TIMEOUT.seconds);
313             pragma(msg, "fixme(pr): consider adding log for exception");
314         }
315         (() @trusted => Thread.sleep(CONTRACT_TIMEOUT.seconds))();
316        
317         return result_ok;
318     }
319 
320     @Then("the contracts should be rejected.")
321     Document rejected() {
322         check(epoch_on_startup, "No epoch on startup");
323         import tagion.dart.DART;
324         auto req = wallet1.getRequestCheckWallet(wallet1_hirpc, used_bills);
325         auto received = sendDARTHiRPC(node1_opts.dart_interface.sock_addr, req, wallet1_hirpc);
326         auto not_in_dart = received.response.result[DART.Params.dart_indices].get!Document[].map!(d => d.get!Buffer).array;
327         check(not_in_dart.length == 0, "all the inputs should still be in the dart");
328 
329 
330         auto output_req = wallet1.getRequestCheckWallet(wallet1_hirpc, output_bills);
331         auto output_received = sendDARTHiRPC(node1_opts.dart_interface.sock_addr, output_req, wallet1_hirpc);
332         auto output_not_in_dart = output_received.response.result[DART.Params.dart_indices].get!Document[].map!(d => d.get!Buffer).array;
333 
334 
335         writefln("wowo OUTPUT %s",output_received.toPretty);
336         check(output_not_in_dart.length == 2, format("No inputs should have been added %s", output_not_in_dart.length));
337 
338         return result_ok;
339     }
340 }
341 
342 @safe @Scenario("Contract where input is smaller than output.",
343         [])
344 class ContractWhereInputIsSmallerThanOutput {
345     Options node1_opts;
346     StdSecureWallet wallet1;
347     SignedContract signed_contract;
348     HiRPC wallet1_hirpc;
349     TagionCurrency start_amount1;
350     bool epoch_on_startup;
351 
352     this(Options opts, ref StdSecureWallet wallet1, bool epoch_on_startup) {
353         this.wallet1 = wallet1;
354         this.node1_opts = opts;
355         wallet1_hirpc = HiRPC(wallet1.net);
356         start_amount1 = wallet1.calcTotal(wallet1.account.bills);
357         this.epoch_on_startup = epoch_on_startup;
358     }
359 
360     @Given("i have a contract where the input bill is smaller than the output bill.")
361     Document bill() {
362         check(epoch_on_startup, "No epoch on startup");
363         auto bill = wallet1.requestBill(100_000.TGN);
364 
365         PayScript pay_script;
366         pay_script.outputs = [bill];
367 
368         const input_bill = wallet1.account.bills[0];
369         wallet1.lock_bills([input_bill]);
370 
371         const nets = wallet1.collectNets([input_bill]);
372         check(nets.all!(net => net !is net.init), "Missing deriver of some of the bills");
373         signed_contract = sign(
374             nets,
375             [input_bill].map!(bill => bill.toDoc).array,
376             null,
377             pay_script.toDoc
378         );
379         return result_ok;
380     }
381 
382     @When("i send the contract to the network.")
383     Document network() {
384         check(epoch_on_startup, "No epoch on startup");
385         sendSubmitHiRPC(node1_opts.inputvalidator.sock_addr, wallet1_hirpc.submit(signed_contract), wallet1.net);
386         return result_ok;
387     }
388 
389     @Then("the contract should be rejected.")
390     Document rejected() {
391         check(epoch_on_startup, "No epoch on startup");
392         auto error = receiveOnlyTimeout!(LogInfo, const(Document))(CONTRACT_TIMEOUT.seconds);
393         return result_ok;
394     }
395 
396 }