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 }