1 /// Service for the tagion virtual machine 
2 /// [Documentation](https://docs.tagion.org/#/documents/architecture/TVM)
3 module tagion.services.TVM;
4 
5 @safe:
6 
7 import core.time;
8 import std.conv : to;
9 import std.stdio;
10 import tagion.actor.actor;
11 import tagion.basic.Debug : __write;
12 import tagion.hibon.Document;
13 import tagion.hibon.HiBONJSON;
14 import tagion.hibon.HiBONRecord;
15 import tagion.logger.Logger;
16 import tagion.logger.Logger;
17 import tagion.script.common;
18 import tagion.script.execute;
19 import tagion.services.messages;
20 import tagion.services.options;
21 
22 enum ResponseError {
23     UnsupportedScript,
24     ExecutionError,
25 }
26 
27 /**
28  * TVMService actor
29  * Receives: 
30  *  (signedContract, immutable(CollectedSignedContract)*)
31  *  (consensusContract, immutable(CollectedSignedContract)*)
32  *
33  * Sends:
34  *  (Payload, const(Document)) to TaskNames.epoch_creator
35  *  (producedContract, immutable(ContractProduct)*) to TaskNames.transcript
36 **/
37 struct TVMService {
38     ActorHandle transcript_handle;
39     ActorHandle epoch_handle;
40 
41     this(immutable(TaskNames) tn) nothrow {
42         transcript_handle = ActorHandle(tn.transcript);
43         epoch_handle = ActorHandle(tn.epoch_creator);
44     }
45 
46     static ContractExecution execute;
47     static Topic tvm_error = Topic("error/tvm");
48 
49     void task() {
50         run(&contract, &consensus_contract);
51     }
52 
53     void contract(signedContract, immutable(CollectedSignedContract)* collected) {
54 
55         if (!engine(collected)) {
56             log(tvm_error, ResponseError.UnsupportedScript.to!string, Document());
57             return;
58         }
59         log("sending pload to epoch creator");
60         epoch_handle.send(Payload(), collected.sign_contract.toDoc);
61     }
62 
63     void consensus_contract(consensusContract, immutable(CollectedSignedContract)* collected) {
64         engine(collected);
65     }
66 
67     bool engine(immutable(CollectedSignedContract)* collected) {
68         log("received signed contract");
69         if (!collected.sign_contract.contract.script.isRecord!PayScript) {
70             log(tvm_error, ResponseError.UnsupportedScript.to!string);
71             return false;
72         }
73         // import std.algorithm;
74         // collected.inputs.each!(d => writefln("%s", d.toPretty));
75 
76         log("before sending to tvm");
77         auto result = execute(collected);
78         if (result.error) {
79             log(tvm_error, ResponseError.ExecutionError.to!string, Document());
80             log.trace("Execution error - %s", result.e.message);
81             return false;
82         }
83         log("sending produced contract to transcript");
84         transcript_handle.send(producedContract(), result.get);
85         return true;
86     }
87 
88 }
89 
90 unittest {
91     import core.time;
92     import tagion.utils.pretend_safe_concurrency;
93 
94     enum task_names = TaskNames();
95     scope (exit) {
96         unregister(task_names.transcript);
97         unregister(task_names.epoch_creator);
98     }
99     register(task_names.transcript, thisTid);
100     register(task_names.epoch_creator, thisTid);
101     auto tvm_service = TVMService(task_names);
102 
103     import std.algorithm.iteration : map;
104     import std.array;
105     import std.range : iota;
106     import tagion.basic.Types : Buffer;
107     import tagion.crypto.Types;
108     import tagion.script.TagionCurrency;
109     import tagion.utils.StdTime;
110 
111     auto createCollected(uint input, uint output) {
112         immutable(Document)[] in_bills;
113         in_bills ~= iota(0, 10).map!(_ => TagionBill(TGN(input), sdt_t.init, Pubkey.init, Buffer.init).toDoc).array;
114         immutable(TagionBill)[] out_bills;
115         out_bills ~= iota(0, 10).map!(_ => TagionBill(TGN(output), sdt_t.init, Pubkey.init, Buffer.init)).array;
116 
117         auto contract = immutable(Contract)(null, null, PayScript(out_bills).toDoc);
118         auto s_contract = new immutable(SignedContract)(null, contract);
119         return new immutable(CollectedSignedContract)(
120                 s_contract,
121                 in_bills,
122                 null,
123         );
124     }
125 
126     { /// Positive test
127         auto collected = createCollected(100, 50);
128         tvm_service.contract(signedContract(), collected);
129 
130         foreach (_; 0 .. 2) {
131             const received = receiveTimeout(
132                     Duration.zero,
133                     (Payload _, const(Document) __) {},
134                     (producedContract _, immutable(ContractProduct)* __) {},
135             );
136             assert(received, "TVM did not send the contract or payload");
137         }
138     }
139 
140     { // False test
141         auto collected = createCollected(50, 100);
142         tvm_service.contract(signedContract(), collected);
143 
144         const received = receiveTimeout(
145                 Duration.zero,
146                 (Payload _, const(Document) __) {},
147                 (producedContract _, immutable(ContractProduct)* __) {},
148         );
149         assert(!received, "The tvm should not send a contract where the output bills are greater than the input");
150     }
151 }