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 }