Skip to content

Commit 72f087f

Browse files
committed
Add Testing section: SC Tester, Arbitrator Tools, Stress Test
New Testing section with three tools: - **SC Tester**: Parse C++ contract headers (paste or URL fetch) and dynamically call functions/procedures at runtime. Supports full struct serialization with MSVC alignment, array fields, identity types, and asset name encoding. - **Arbitrator Tools**: Testnet-only tools for broadcasting computor lists and raw packets, using the active seed session for signing. - **Stress Test**: Network load testing with Quick Test and Playbook modes. Quick Test runs a single test type; Playbook defines multi-step sequences that can be saved/loaded as JSON files. Test types: multi-seed TX flood with random distribution, SC query flood, message flood, raw packet injection. Live stats with per-step progress tracking. Also includes: - DynamicContractService for runtime C++ header parsing and binary serialization via linked ContractGen source files - ContractGen fixes: regex-based suffix stripping, #define support, single-line empty struct handling, ParseText() overload - NavMenu and Dashboard updates for the new section
1 parent cd601b6 commit 72f087f

File tree

10 files changed

+2336
-2
lines changed

10 files changed

+2336
-2
lines changed

Components/Layout/NavMenu.razor

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,25 @@
168168
}
169169
}
170170

171+
@* ── TESTING ── *@
172+
<div class="nav-section-header">
173+
<a class="nav-section-label" href="/testing">TESTING</a>
174+
<span class="nav-section-toggle" @onclick='() => Toggle("testing")'>
175+
<i class="bi bi-chevron-right chevron @(IsExpanded("testing") ? "expanded" : "")"></i>
176+
</span>
177+
</div>
178+
@if (IsExpanded("testing"))
179+
{
180+
<div class="nav-sub-links">
181+
@NavItem("/testing/sc-tester", "bi-file-code", "SC Tester")
182+
@if (IsDirectNetwork)
183+
{
184+
@NavItem("/testing/arbitrator", "bi-shield-lock", "Arbitrator Tools")
185+
@NavItem("/testing/stress-test", "bi-lightning-charge", "Stress Test")
186+
}
187+
</div>
188+
}
189+
171190
<hr class="my-2" />
172191
<NavLink class="nav-link" href="/settings">
173192
<i class="bi bi-gear me-2"></i>Settings
@@ -222,6 +241,9 @@
222241
["/tx/ipobid"] = ("IPO Bid", "bi-cash-stack"),
223242
["/node/peers"] = ("Node Peers", "bi-diagram-3"),
224243
["/node/management"] = ("Node Management", "bi-sliders"),
244+
["/testing/sc-tester"] = ("SC Tester", "bi-file-code"),
245+
["/testing/arbitrator"] = ("Arbitrator Tools", "bi-shield-lock"),
246+
["/testing/stress-test"] = ("Stress Test", "bi-lightning-charge"),
225247
};
226248

227249
private static readonly Dictionary<string, string[]> SectionRoutes = new()
@@ -231,6 +253,7 @@
231253
["explorer"] = ["/explorer", "/explorer/", "/wallet/balance", "/wallet/assets", "/tx/lookup", "/wallet/transfers", "/explorer/tick", "/explorer/computors", "/explorer/ipos", "/explorer/ipostatus", "/explorer/checktx", "/explorer/logs"],
232254
["tools"] = ["/tools", "/tools/", "/wallet/identity", "/tx/import", "/tools/crypto", "/tools/oracle", "/tools/bob-playground"],
233255
["computor"] = ["/node", "/node/", "/tx/governance", "/tx/ccf", "/node/peers", "/node/management"],
256+
["testing"] = ["/testing", "/testing/", "/testing/sc-tester", "/testing/arbitrator", "/testing/stress-test"],
234257
};
235258

236259
protected override void OnInitialized()

Components/Pages/Dashboard.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@
152152
@BackendRow("CCF", "/tx/ccf", true, false, false, icon: "bi-currency-exchange")
153153
@BackendRow("Node Peers", "/node/peers", false, false, true, icon: "bi-diagram-3")
154154
@BackendRow("Node Management", "/node/management", false, false, true, icon: "bi-sliders")
155+
156+
<tr class="table-secondary"><td colspan="4"><strong>Testing</strong></td></tr>
157+
@BackendRow("SC Tester", "/testing/sc-tester", true, true, true, icon: "bi-file-code")
158+
@BackendRow("Arbitrator Tools", "/testing/arbitrator", false, false, true, icon: "bi-shield-lock")
159+
@BackendRow("Stress Test", "/testing/stress-test", false, false, true, icon: "bi-lightning-charge")
155160
</tbody>
156161
</table>
157162
</div>
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
@page "/testing/arbitrator"
2+
@using System.Buffers.Binary
3+
@using Qubic.Core.Entities
4+
@using Qubic.Serialization
5+
@inject QubicBackendService Backend
6+
@inject SeedSessionService Seed
7+
8+
<h4><i class="bi bi-shield-lock me-2"></i>Arbitrator Tools</h4>
9+
<p class="text-muted">Testnet-only: issue computor lists and manage epoch lifecycle.</p>
10+
11+
@if (Backend.ActiveBackend != QueryBackend.DirectNetwork)
12+
{
13+
<div class="alert alert-warning">This tool requires DirectNetwork backend. Switch in Settings.</div>
14+
return;
15+
}
16+
17+
@if (!Seed.HasSeed)
18+
{
19+
<div class="alert alert-warning">No seed session active. Open a seed session in Settings first.</div>
20+
return;
21+
}
22+
23+
@* ── ARBITRATOR IDENTITY ── *@
24+
<div class="card mb-3">
25+
<div class="card-header">Arbitrator Identity</div>
26+
<div class="card-body">
27+
<div class="small">Identity: <span class="mono">@Seed.Identity</span></div>
28+
@if (Seed.ActiveLabel != null)
29+
{
30+
<div class="small text-muted">Label: @Seed.ActiveLabel</div>
31+
}
32+
</div>
33+
</div>
34+
35+
@* ── BROADCAST COMPUTOR LIST ── *@
36+
<div class="card mb-3">
37+
<div class="card-header">Broadcast Computor List</div>
38+
<div class="card-body">
39+
<div class="row g-2 mb-2">
40+
<div class="col-md-4">
41+
<label class="form-label small mb-0">Epoch</label>
42+
<input type="number" class="form-control form-control-sm" @bind="_epoch" />
43+
</div>
44+
</div>
45+
46+
<label class="form-label small mb-0">Computor Identities (676 total)</label>
47+
<div class="d-flex gap-2 mb-1">
48+
<button class="btn btn-sm btn-outline-secondary" @onclick="GenerateRandomComputors">
49+
Generate 676 Random
50+
</button>
51+
<button class="btn btn-sm btn-outline-secondary" @onclick="FillWithSessionIdentity">
52+
Fill With Session Identity
53+
</button>
54+
</div>
55+
<textarea class="form-control mono" rows="8" @bind="_computorListText"
56+
placeholder="One identity per line (676 total)..."></textarea>
57+
<div class="small text-muted mt-1">@ComputorCount / @NumComputors identities</div>
58+
59+
<button class="btn btn-primary mt-2" @onclick="BroadcastComputors"
60+
disabled="@_broadcasting">
61+
@(_broadcasting ? "Broadcasting..." : "Broadcast Computor List")
62+
</button>
63+
64+
@if (_broadcastResult != null)
65+
{
66+
<div class="alert @(_broadcastSuccess ? "alert-success" : "alert-danger") mt-2 py-1 px-2 small mb-0">
67+
@_broadcastResult
68+
</div>
69+
}
70+
</div>
71+
</div>
72+
73+
@* ── RAW BROADCAST ── *@
74+
<div class="card mb-3">
75+
<div class="card-header">Raw Packet Broadcast</div>
76+
<div class="card-body">
77+
<div class="row g-2 mb-2">
78+
<div class="col-md-3">
79+
<label class="form-label small mb-0">Packet Type</label>
80+
<input type="number" class="form-control form-control-sm" @bind="_rawPacketType" min="0" max="255" />
81+
</div>
82+
<div class="col-md-9">
83+
<label class="form-label small mb-0">Payload (hex)</label>
84+
<input class="form-control form-control-sm mono" @bind="_rawPayloadHex"
85+
placeholder="Payload bytes in hex (without header)" />
86+
</div>
87+
</div>
88+
<button class="btn btn-sm btn-outline-danger" @onclick="SendRawPacket"
89+
disabled="@_sendingRaw">
90+
@(_sendingRaw ? "Sending..." : "Send Packet")
91+
</button>
92+
@if (_rawResult != null)
93+
{
94+
<div class="alert @(_rawSuccess ? "alert-success" : "alert-danger") mt-2 py-1 px-2 small mb-0">
95+
@_rawResult
96+
</div>
97+
}
98+
</div>
99+
</div>
100+
101+
@code {
102+
private const int NumComputors = 676;
103+
104+
private int _epoch = 1;
105+
private string _computorListText = "";
106+
private bool _broadcasting;
107+
private bool _broadcastSuccess;
108+
private string? _broadcastResult;
109+
110+
private int _rawPacketType = 0;
111+
private string _rawPayloadHex = "";
112+
private bool _sendingRaw;
113+
private bool _rawSuccess;
114+
private string? _rawResult;
115+
116+
private int ComputorCount => string.IsNullOrWhiteSpace(_computorListText) ? 0
117+
: _computorListText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Length;
118+
119+
private void GenerateRandomComputors()
120+
{
121+
var lines = new string[NumComputors];
122+
var crypt = new Qubic.Crypto.QubicCrypt();
123+
for (int i = 0; i < NumComputors; i++)
124+
{
125+
var seed = new string(Enumerable.Range(0, 55).Select(_ => (char)('a' + Random.Shared.Next(26))).ToArray());
126+
var pk = crypt.GetPublicKey(seed);
127+
lines[i] = QubicIdentity.FromPublicKey(pk).Identity;
128+
}
129+
_computorListText = string.Join('\n', lines);
130+
}
131+
132+
private void FillWithSessionIdentity()
133+
{
134+
var identity = Seed.Identity?.ToString();
135+
if (string.IsNullOrEmpty(identity)) return;
136+
_computorListText = string.Join('\n', Enumerable.Repeat(identity, NumComputors));
137+
}
138+
139+
private async Task BroadcastComputors()
140+
{
141+
_broadcasting = true;
142+
_broadcastResult = null;
143+
144+
try
145+
{
146+
var identities = _computorListText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
147+
148+
if (identities.Length < NumComputors)
149+
{
150+
_broadcastResult = $"Need {NumComputors} identities, got {identities.Length}.";
151+
_broadcastSuccess = false;
152+
return;
153+
}
154+
155+
// Build Computors payload: epoch(2) + publicKeys(676*32) + signature(64)
156+
var dataSize = 2 + NumComputors * 32;
157+
var totalSize = dataSize + 64;
158+
var computorsData = new byte[totalSize];
159+
160+
// Write epoch (LE uint16)
161+
BinaryPrimitives.WriteUInt16LittleEndian(computorsData.AsSpan(0), (ushort)_epoch);
162+
163+
// Write 676 public keys
164+
for (int i = 0; i < NumComputors; i++)
165+
{
166+
var pubKey = QubicIdentity.FromIdentity(identities[i].Trim()).PublicKey;
167+
pubKey.CopyTo(computorsData.AsSpan(2 + i * 32));
168+
}
169+
170+
// Sign the epoch + publicKeys portion using the seed session
171+
var signature = Seed.SignMessage(computorsData.AsSpan(0, dataSize).ToArray());
172+
signature.CopyTo(computorsData.AsSpan(dataSize));
173+
174+
// Build packet: header(8) + computorsData
175+
var packetSize = QubicPacketHeader.Size + computorsData.Length;
176+
var packet = new byte[packetSize];
177+
178+
// Write broadcast header with dejavu=0 for propagation
179+
var header = QubicPacketHeader.Create(QubicPacketTypes.BroadcastComputors, computorsData.Length, dejavu: 0);
180+
uint sizeAndType = (uint)header.PacketSize | ((uint)header.Type << 24);
181+
BinaryPrimitives.WriteUInt32LittleEndian(packet.AsSpan(0), sizeAndType);
182+
BinaryPrimitives.WriteUInt32LittleEndian(packet.AsSpan(4), header.Dejavu);
183+
computorsData.CopyTo(packet.AsSpan(QubicPacketHeader.Size));
184+
185+
await Backend.SendRawPacketAsync(packet);
186+
_broadcastSuccess = true;
187+
_broadcastResult = $"Computor list broadcast sent ({packet.Length} bytes) for epoch {_epoch}.";
188+
}
189+
catch (Exception ex)
190+
{
191+
_broadcastSuccess = false;
192+
_broadcastResult = ex.Message;
193+
}
194+
finally
195+
{
196+
_broadcasting = false;
197+
}
198+
}
199+
200+
private async Task SendRawPacket()
201+
{
202+
_sendingRaw = true;
203+
_rawResult = null;
204+
205+
try
206+
{
207+
var payload = string.IsNullOrWhiteSpace(_rawPayloadHex) ? Array.Empty<byte>()
208+
: Convert.FromHexString(_rawPayloadHex.Trim());
209+
210+
var packetSize = QubicPacketHeader.Size + payload.Length;
211+
var packet = new byte[packetSize];
212+
213+
var header = QubicPacketHeader.Create((byte)_rawPacketType, payload.Length, dejavu: 0);
214+
uint sizeAndType = (uint)header.PacketSize | ((uint)header.Type << 24);
215+
BinaryPrimitives.WriteUInt32LittleEndian(packet.AsSpan(0), sizeAndType);
216+
BinaryPrimitives.WriteUInt32LittleEndian(packet.AsSpan(4), header.Dejavu);
217+
if (payload.Length > 0)
218+
payload.CopyTo(packet.AsSpan(QubicPacketHeader.Size));
219+
220+
await Backend.SendRawPacketAsync(packet);
221+
_rawSuccess = true;
222+
_rawResult = $"Packet sent: type={_rawPacketType}, {packet.Length} bytes total.";
223+
}
224+
catch (Exception ex)
225+
{
226+
_rawSuccess = false;
227+
_rawResult = ex.Message;
228+
}
229+
finally
230+
{
231+
_sendingRaw = false;
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)