1 module tagion.testbench.services.genesis_test;
2 // Default import list for bdd
3 import std.typecons : Tuple;
4 import tagion.behaviour;
5 import tagion.hibon.Document;
6 import tagion.testbench.tools.Environment;
7 import std.stdio;
8 import tagion.logger.LogRecords : LogInfo;
9 import tagion.logger.Logger;
10 import tagion.hashgraph.Refinement;
11 import tagion.utils.pretend_safe_concurrency;
12 import tagion.testbench.actor.util;
13 import core.time;
14 import std.range;
15 import std.array;
16 import tagion.dart.Recorder;
17 import tagion.services.replicator : modify_log;
18 import tagion.actor;
19 import tagion.services.options;
20 import tagion.crypto.SecureInterfaceNet;
21 import std.format;
22 import tagion.hibon.HiBONRecord : isRecord;
23 import tagion.script.common;
24 import tagion.crypto.SecureNet : StdHashNet;
25 import std.algorithm;
26 import core.thread;
27 import std.exception;
28 import tagion.crypto.Types;
29 import tagion.script.TagionCurrency;
30 import tagion.tools.wallet.WalletInterface;
31 import tagion.crypto.SecureNet : StdSecureNet;
32 import tagion.wallet.SecureWallet : SecureWallet;
33 import tagion.basic.Types : Buffer;
34 import tagion.utils.StdTime;
35 import tagion.communication.HiRPC;
36 import tagion.hibon.HiBONtoText;
37 
38 enum EPOCH_TIMEOUT = 15;
39 alias StdSecureWallet = SecureWallet!StdSecureNet;
40 
41 enum feature = Feature(
42             "Boot system with genesis block.",
43             []);
44 
45 alias FeatureContext = Tuple!(
46         NetworkRunningWithGenesisBlockAndEpochChain, "NetworkRunningWithGenesisBlockAndEpochChain",
47         FeatureGroup*, "result"
48 );
49 
50 
51 
52 
53 
54 
55 @safe @Scenario("network running with genesis block and epoch chain.",
56         [])
57 class NetworkRunningWithGenesisBlockAndEpochChain {
58     bool epoch_on_startup;
59     long start_epoch;
60     RecordFactory record_factory;
61     Options[] opts;
62     ActorHandle[] handles;
63     HashNet net = new StdHashNet;
64     StdSecureWallet wallet1;
65     TagionCurrency amount;
66     TagionCurrency fee;
67 
68     const(GenesisEpoch) genesis_epoch;
69     SignedContract signed_contract;
70 
71     
72     struct History {
73         TagionHead[] heads;
74         Epoch[] epochs;
75     }
76     History[string] histories;
77 
78     this(Options[] opts, ref StdSecureWallet wallet1, const(GenesisEpoch) genesis_epoch) {
79         this.opts = opts;
80         record_factory = RecordFactory(net);
81         this.wallet1 = wallet1;
82         this.genesis_epoch = genesis_epoch;
83     }
84 
85     @Given("i have a network booted with a genesis block")
86     Document block() {
87 
88         amount = 500.TGN;
89         auto bill_to_pay = TagionBill(amount, currentTime, Pubkey([0]), Buffer.init);
90         check(wallet1.createPayment([bill_to_pay], signed_contract, fee).value, "Error creating payment");
91         check(signed_contract.contract.inputs.length == 1, format("should only contain one bill had %s", signed_contract.contract.inputs));
92 
93         auto wallet1_hirpc = HiRPC(wallet1.net);
94         auto hirpc_submit = wallet1_hirpc.submit(signed_contract);
95 
96         foreach(opt; opts) {
97             histories[opt.task_names.replicator] = History.init;
98         }
99 
100         writefln("signed_contract: %s", signed_contract.toPretty);
101 
102 
103         submask.subscribe(modify_log);
104 
105         int max = 100;
106         int start = 0;
107         while(start < max) {
108             if (start == 20) {
109                 sendSubmitHiRPC(opts[0].inputvalidator.sock_addr, hirpc_submit, wallet1.net);
110             }
111             auto modify_log_result = receiveOnlyTimeout!(LogInfo, const(Document))(EPOCH_TIMEOUT.seconds);
112             log("received something");
113 
114             check(modify_log_result[1].isRecord!(RecordFactory.Recorder), "Did not receive recorder");
115             // writefln("received recorder %s", modify_log_result[1].toPretty);
116 
117             auto recorder = record_factory.recorder(modify_log_result[1]);
118             auto head = recorder[].filter!(a => a.filed.isRecord!TagionHead).array;
119             check(head.length == 1, format("Should contain only one head per modify. had %s", head.length));
120 
121 
122             const task_name = modify_log_result[0].task_name;
123             
124             histories[task_name].heads ~= TagionHead(head.front.filed); 
125 
126             auto epochs = recorder[]
127                     .filter!(a => a.filed.isRecord!Epoch)
128                     .map!(a => Epoch(a.filed))
129                     .filter!(e => !e.previous.empty)
130                     .array;
131 
132             histories[task_name].epochs ~= epochs;
133             writefln("EPOCH NUMBERS %s", epochs.map!(e => format("%(%02x%)", e.previous)).array);
134 
135 
136             start++;
137         }
138         // (() @trusted => writefln("%s", histories))();
139 
140         return result_ok;
141     }
142 
143     @When("the network continues to run.")
144     Document run() @trusted {
145         // start by sorting the histories
146 
147         History[string] sorted_histories;
148 
149         foreach(hist; histories.byKeyValue) {
150             auto sorted_epochs = hist.value.epochs.sort!((a,b) => a.epoch_number < b.epoch_number).array;
151             auto sorted_heads = hist.value.heads.sort!((a,b) => a.current_epoch < b.current_epoch).array;
152             check(sorted_epochs.length <= sorted_heads.length, format("there should be equal or more heads than the total amount of epochs, heads %s, epochs %s", sorted_heads.length, sorted_epochs.length));
153             History sorted_hist;
154             sorted_hist.epochs = sorted_epochs;
155             sorted_hist.heads = sorted_heads;
156             sorted_histories[hist.key] = sorted_hist;
157         }
158 
159         // since we need to ref to the previous element we use a for loop over the epochs since we know that there must be fewer epochs than heads.
160 
161         Epoch[] ref_epochs = sorted_histories.byValue.front.epochs.array;
162         check(ref_epochs.length > 0, "did not create any finished epochs");
163 
164         // we first check that the epoch for the first is node created correctly. Then we compare the different epochs afterwards
165         for (int i = 1; i < ref_epochs.length; i++) {
166             auto ref_epoch = ref_epochs[i];
167             auto prev_epoch = ref_epochs[i-1];
168 
169             writefln("comparing %s", i);
170             
171 
172 
173             
174             writefln("prev epoch: %s \n hash_of_prev: %s\n new epoch: %s", prev_epoch.toPretty, net.calcHash(prev_epoch).encodeBase64, ref_epoch.toPretty);
175             check(ref_epoch.epoch_number == prev_epoch.epoch_number +1, "The epoch number was not correctly incremented");
176             auto previous = net.calcHash(prev_epoch);
177             check(previous == ref_epoch.previous, format("The fingerprint was not correct. should be %s was %s", previous.encodeBase64, ref_epoch.previous.encodeBase64));
178 
179             if (ref_epoch.globals != prev_epoch.globals) {
180                 writefln("NEW DIF EPOCH %s\n PREV EPOCH %s", ref_epoch.globals.toPretty, prev_epoch.globals.toPretty);
181 
182                 
183                 // this was the epoch where our tx should have gone through
184                 check(ref_epoch.globals.total < prev_epoch.globals.total, format("the total was not decreased previous %s current %s", prev_epoch.globals.total, ref_epoch.globals.total));
185 
186 
187                 const(TagionBill)[] outputs = PayScript(signed_contract.contract.script).outputs;
188                 const(TagionBill)[] inputs = [wallet1.account.bills.front];
189                 auto delta_bills = outputs.length - inputs.length; 
190                 check(ref_epoch.globals.burnt_bills - 1 == prev_epoch.globals.burnt_bills, "the contract should have burned a bill");
191 
192                 TagionCurrency burned = wallet1.calcTotal(inputs) - wallet1.calcTotal(outputs);
193                 check(burned > 0, "should have burned more for the contract");
194 
195                 check(prev_epoch.globals.total_burned + wallet1.calcTotal(inputs).units == ref_epoch.globals.total_burned, format("the burned amount was not correct. prev_epoch burned: %s, new_epoch burned %s burned units %s", prev_epoch.globals.total_burned, ref_epoch.globals.total_burned, burned.units));
196                 check(ref_epoch.globals.number_of_bills - delta_bills == prev_epoch.globals.number_of_bills, "We should have updated the number of bills");
197             }
198 
199             foreach(hist; sorted_histories.byValue) {
200                 if (hist.epochs.length-1 < i) {
201                     continue;
202                 } 
203                 check(hist.epochs[i] == ref_epoch, "The epoch was different across the nodes");
204             }
205         }
206         return result_ok;
207     }
208 
209 
210 
211     @Then("it should continue adding blocks to the _epochchain")
212     Document epochchain() {
213         return result_ok;
214     }
215 
216     @Then("check the chains validity.")
217     Document validity() {
218         return result_ok;
219     }
220 
221 }
222