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 }