Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4d0ac855c | |||
| eb1c7d649e | |||
| 75f34ef51b | |||
| 4f7fa557d5 | |||
| a96fb098c2 | |||
| ad5c6ab829 | |||
| b3d07e8832 | |||
| e761255174 | |||
| e4a20f9cf5 | |||
| 1eefe8a579 |
Generated
+2
-2
@@ -4731,7 +4731,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.57"
|
||||
version = "1.1.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -6038,7 +6038,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitor"
|
||||
version = "1.1.0"
|
||||
version = "1.1.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5"
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_key, serialization_revision as \"serialization_revision: u8\"\n FROM master_verification_key WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "serialised_key",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as count FROM simulated_node_performance WHERE simulated_epoch_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0557e64c547e147ef7eef713b49c62afde62b3b04ff817ae372ffe9fbeaee6b8"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as count FROM simulated_reward_epochs",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1411660a6456f6f5ca141db10d9a54857fc8b4a661e09ae6719d84ac2e77cf6d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?);\n UPDATE expiration_date_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE expiration_date = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", epoch_id as \"epoch_id!: u32\", calculation_method as \"calculation_method!\", \n start_timestamp as \"start_timestamp!\", end_timestamp as \"end_timestamp!\", \n description, created_at as \"created_at!\"\n FROM simulated_reward_epochs\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id!: u32",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "start_timestamp!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "end_timestamp!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1e4daea4d5f87938cf98cb2b6facc5802e79b06f3d0d41f6467844265b26d0f7"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO ecash_ticketbook\n (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO simulated_reward_epochs \n (epoch_id, calculation_method, start_timestamp, end_timestamp, description)\n VALUES (?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2bb062a25129bfc6b060f68592e74aa91d8c82b8b912009fce14f9c19fd4464c"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", \n node_id as \"node_id!: NodeId\", calculation_method as \"calculation_method!\", \n performance_rank as \"performance_rank!\", performance_percentile as \"performance_percentile!\",\n calculated_at as \"calculated_at!\"\n FROM simulated_performance_rankings\n WHERE simulated_epoch_id = ? AND calculation_method = ?\n ORDER BY performance_rank\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "performance_rank!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "performance_percentile!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3024d5e589b1f50f95f5e7bf09e31161beca74e137e340d2a6a02a589ad251cb"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", \n node_id as \"node_id!: NodeId\", calculation_method as \"calculation_method!\", \n performance_rank as \"performance_rank!\", performance_percentile as \"performance_percentile!\",\n calculated_at as \"calculated_at!\"\n FROM simulated_performance_rankings\n WHERE simulated_epoch_id = ?\n ORDER BY calculation_method, performance_rank\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "performance_rank!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "performance_percentile!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "33108405de0328977ac5d05d89f484bdb127f9cba0ba5370c931e1225983d74a"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4"
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT sra.id as \"id!\", sra.simulated_epoch_id as \"simulated_epoch_id!\", \n sra.calculation_method as \"calculation_method!\", sra.total_routes_analyzed as \"total_routes_analyzed!: u32\", \n sra.successful_routes as \"successful_routes!: u32\", sra.failed_routes as \"failed_routes!: u32\", \n sra.average_route_reliability, sra.time_window_hours as \"time_window_hours!: u32\", \n sra.analysis_parameters, sra.calculated_at as \"calculated_at!\"\n FROM simulated_route_analysis sra\n JOIN simulated_reward_epochs sre ON sra.simulated_epoch_id = sre.id\n WHERE sre.epoch_id = ? AND sra.calculation_method = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "total_routes_analyzed!: u32",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "successful_routes!: u32",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "failed_routes!: u32",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "average_route_reliability",
|
||||
"ordinal": 6,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "time_window_hours!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "analysis_parameters",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4e2338362058e4356ac639fdf806e827761f879cab723cb2a649bc76b176fdad"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", node_id as \"node_id!: NodeId\", \n node_type as \"node_type!\", performance_score as \"performance_score!\", \n work_factor as \"work_factor!\", calculation_method as \"calculation_method!\", \n positive_samples as \"positive_samples?\", negative_samples as \"negative_samples?\",\n route_success_rate as \"route_success_rate?\", calculated_at as \"calculated_at!\"\n FROM simulated_performance_comparisons\n WHERE simulated_epoch_id = ?\n ORDER BY calculation_method, node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "performance_score!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "work_factor!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples?",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples?",
|
||||
"ordinal": 8,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "route_success_rate?",
|
||||
"ordinal": 9,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6a7b25d2c580298bd44fcf74e4d2c550537c64dbfb2c17a6c91bd5345b9a3e6a"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT snp.id as \"id!\", snp.simulated_epoch_id as \"simulated_epoch_id!\", \n snp.node_id as \"node_id!: NodeId\", snp.node_type as \"node_type!\", \n snp.identity_key, snp.reliability_score as \"reliability_score!\", \n snp.positive_samples as \"positive_samples!: u32\", snp.negative_samples as \"negative_samples!: u32\", \n snp.work_factor, snp.calculation_method as \"calculation_method!\", snp.calculated_at as \"calculated_at!\"\n FROM simulated_node_performance snp\n JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id\n WHERE snp.node_id = ?\n ORDER BY sre.epoch_id DESC, snp.calculation_method\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6fa6967e77886b69b049c68b664cdf8f4b97daa3734036db16e49915fd89073a"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO pending_issuance\n (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)\n VALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);\n UPDATE master_verification_key\n SET\n serialised_key = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e"
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM coin_indices_signatures WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE ecash_ticketbook\n SET used_tickets = used_tickets - ?\n WHERE id = ?\n AND used_tickets = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);\n UPDATE coin_indices_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", node_id as \"node_id!: NodeId\", \n node_type as \"node_type!\", identity_key, reliability_score as \"reliability_score!\", \n positive_samples as \"positive_samples!: u32\", negative_samples as \"negative_samples!: u32\", \n work_factor, calculation_method as \"calculation_method!\", calculated_at as \"calculated_at!\"\n FROM simulated_node_performance\n WHERE simulated_epoch_id = ?\n ORDER BY calculation_method, node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c04c33631eea6959267dd441f50c2bd876ba951e32e8b8bee398cf98fec5e7bf"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT DISTINCT calculation_method FROM simulated_reward_epochs WHERE epoch_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "calculation_method",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c9796e61c49a29ebd42dec46fb047f7a7e2f80aa4db4f963146cf56864a62347"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", node_id as \"node_id!: NodeId\", \n node_type as \"node_type!\", identity_key, reliability_score as \"reliability_score!\", \n positive_samples as \"positive_samples!: u32\", negative_samples as \"negative_samples!: u32\", \n work_factor, calculation_method as \"calculation_method!\", calculated_at as \"calculated_at!\"\n FROM simulated_node_performance\n WHERE simulated_epoch_id = ? AND calculation_method = ?\n ORDER BY node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d76e131fff79978284f31509e70d42d20ea2e95e5a2f2d574d33ad8d2a7f2a19"
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", \n calculation_method as \"calculation_method!\", total_routes_analyzed as \"total_routes_analyzed!: u32\", \n successful_routes as \"successful_routes!: u32\", failed_routes as \"failed_routes!: u32\", \n average_route_reliability, time_window_hours as \"time_window_hours!: u32\", \n analysis_parameters, calculated_at as \"calculated_at!\"\n FROM simulated_route_analysis\n WHERE simulated_epoch_id = ?\n ORDER BY calculation_method\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "total_routes_analyzed!: u32",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "successful_routes!: u32",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "failed_routes!: u32",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "average_route_reliability",
|
||||
"ordinal": 6,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "time_window_hours!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "analysis_parameters",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "db2b71d1dd33022d5c960053d6a0f5d7ccf52de6775d4f57744d1401d44fee62"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!\", epoch_id as \"epoch_id!: u32\", calculation_method as \"calculation_method!\", \n start_timestamp as \"start_timestamp!\", end_timestamp as \"end_timestamp!\", \n description, created_at as \"created_at!\"\n FROM simulated_reward_epochs \n ORDER BY created_at DESC \n LIMIT ? OFFSET ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id!: u32",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "start_timestamp!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "end_timestamp!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dbc52e443ab600516b4f96ab2904d6b6379c06cfa1ff8598c29b67a2fe37055d"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO simulated_route_analysis\n (simulated_epoch_id, calculation_method, total_routes_analyzed, successful_routes,\n failed_routes, average_route_reliability, time_window_hours, analysis_parameters)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "df9013912c0e4177625267776656cb139cf3ac8dd4cd4c5146005b055aefd228"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT snp.id as \"id!\", snp.simulated_epoch_id as \"simulated_epoch_id!\", \n snp.node_id as \"node_id!: NodeId\", snp.node_type as \"node_type!\", \n snp.identity_key, snp.reliability_score as \"reliability_score!\", \n snp.positive_samples as \"positive_samples!: u32\", snp.negative_samples as \"negative_samples!: u32\", \n snp.work_factor, snp.calculation_method as \"calculation_method!\", snp.calculated_at as \"calculated_at!\"\n FROM simulated_node_performance snp\n JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id\n WHERE sre.epoch_id = ? AND snp.calculation_method = ?\n ORDER BY snp.node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e2a8ebf260e0e3dd25d614a3c9f70715d0b593cf19d0a5f5c7972ac5925151c3"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", node_id as \"node_id!: NodeId\", \n node_type as \"node_type!\", performance_score as \"performance_score!\", \n work_factor as \"work_factor!\", calculation_method as \"calculation_method!\", \n positive_samples as \"positive_samples?\", negative_samples as \"negative_samples?\",\n route_success_rate as \"route_success_rate?\", calculated_at as \"calculated_at!\"\n FROM simulated_performance_comparisons\n WHERE simulated_epoch_id = ? AND calculation_method = ?\n ORDER BY node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "performance_score!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "work_factor!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples?",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples?",
|
||||
"ordinal": 8,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "route_success_rate?",
|
||||
"ordinal": 9,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e8a88007b46202c6dcd87c2f964e366fa2b6fe0c0fd71ce48a544d51c679a9c8"
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.60"
|
||||
version = "1.1.61"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
-- Migration: Add performance methodology comparison tables
|
||||
-- This migration adds support for comparing performance calculation methodologies:
|
||||
-- old (24h cache-based) vs new (1h route-based) without affecting actual rewards
|
||||
|
||||
-- Simulated reward epochs track each simulation run
|
||||
CREATE TABLE IF NOT EXISTS simulated_reward_epochs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
epoch_id INTEGER NOT NULL,
|
||||
calculation_method TEXT NOT NULL CHECK (calculation_method IN ('old', 'new', 'comparison', 'test')),
|
||||
start_timestamp INTEGER NOT NULL,
|
||||
end_timestamp INTEGER NOT NULL,
|
||||
description TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
-- Node performance data calculated from different methodologies
|
||||
CREATE TABLE IF NOT EXISTS simulated_node_performance (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
simulated_epoch_id INTEGER NOT NULL,
|
||||
node_id INTEGER NOT NULL,
|
||||
node_type TEXT NOT NULL CHECK (node_type IN ('mixnode', 'gateway')),
|
||||
identity_key TEXT,
|
||||
reliability_score REAL NOT NULL CHECK (reliability_score >= 0.0 AND reliability_score <= 100.0),
|
||||
positive_samples INTEGER NOT NULL DEFAULT 0,
|
||||
negative_samples INTEGER NOT NULL DEFAULT 0,
|
||||
work_factor REAL CHECK (work_factor >= 0.0 AND work_factor <= 1.0),
|
||||
calculation_method TEXT NOT NULL CHECK (calculation_method IN ('old', 'new', 'test')),
|
||||
calculated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (simulated_epoch_id) REFERENCES simulated_reward_epochs(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Performance comparison data for analyzing methodology differences
|
||||
CREATE TABLE IF NOT EXISTS simulated_performance_comparisons (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
simulated_epoch_id INTEGER NOT NULL,
|
||||
node_id INTEGER NOT NULL,
|
||||
node_type TEXT NOT NULL CHECK (node_type IN ('mixnode', 'gateway')),
|
||||
-- Performance scores from each methodology
|
||||
performance_score REAL NOT NULL CHECK (performance_score >= 0.0 AND performance_score <= 100.0),
|
||||
work_factor REAL NOT NULL CHECK (work_factor >= 0.0),
|
||||
calculation_method TEXT NOT NULL CHECK (calculation_method IN ('old', 'new', 'test')),
|
||||
-- Additional metrics for analysis
|
||||
positive_samples INTEGER DEFAULT 0,
|
||||
negative_samples INTEGER DEFAULT 0,
|
||||
route_success_rate REAL CHECK (route_success_rate >= 0.0 AND route_success_rate <= 100.0),
|
||||
calculated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (simulated_epoch_id) REFERENCES simulated_reward_epochs(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Route analysis metadata for each simulation run
|
||||
CREATE TABLE IF NOT EXISTS simulated_route_analysis (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
simulated_epoch_id INTEGER NOT NULL,
|
||||
calculation_method TEXT NOT NULL CHECK (calculation_method IN ('old', 'new', 'test')),
|
||||
total_routes_analyzed INTEGER NOT NULL DEFAULT 0,
|
||||
successful_routes INTEGER NOT NULL DEFAULT 0,
|
||||
failed_routes INTEGER NOT NULL DEFAULT 0,
|
||||
average_route_reliability REAL CHECK (average_route_reliability >= 0.0 AND average_route_reliability <= 100.0),
|
||||
time_window_hours INTEGER NOT NULL DEFAULT 1, -- New method uses 1 hour, old uses 24
|
||||
analysis_parameters TEXT, -- JSON with additional analysis configuration
|
||||
calculated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (simulated_epoch_id) REFERENCES simulated_reward_epochs(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Performance rankings and comparison analytics
|
||||
CREATE TABLE IF NOT EXISTS simulated_performance_rankings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
simulated_epoch_id INTEGER NOT NULL,
|
||||
node_id INTEGER NOT NULL,
|
||||
calculation_method TEXT NOT NULL CHECK (calculation_method IN ('old', 'new', 'test')),
|
||||
performance_rank INTEGER NOT NULL,
|
||||
performance_percentile REAL NOT NULL CHECK (performance_percentile >= 0.0 AND performance_percentile <= 100.0),
|
||||
calculated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (simulated_epoch_id) REFERENCES simulated_reward_epochs(id) ON DELETE CASCADE,
|
||||
UNIQUE(simulated_epoch_id, node_id, calculation_method)
|
||||
);
|
||||
|
||||
-- Indexes for efficient querying
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_reward_epochs_epoch_id ON simulated_reward_epochs(epoch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_reward_epochs_calculation_method ON simulated_reward_epochs(calculation_method);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_reward_epochs_created_at ON simulated_reward_epochs(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_node_performance_simulated_epoch_id ON simulated_node_performance(simulated_epoch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_node_performance_node_id ON simulated_node_performance(node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_node_performance_calculation_method ON simulated_node_performance(calculation_method);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_node_performance_node_type ON simulated_node_performance(node_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_comparisons_simulated_epoch_id ON simulated_performance_comparisons(simulated_epoch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_comparisons_node_id ON simulated_performance_comparisons(node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_comparisons_calculation_method ON simulated_performance_comparisons(calculation_method);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_comparisons_node_type ON simulated_performance_comparisons(node_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_comparisons_performance_score ON simulated_performance_comparisons(performance_score);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_route_analysis_simulated_epoch_id ON simulated_route_analysis(simulated_epoch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_route_analysis_calculation_method ON simulated_route_analysis(calculation_method);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_rankings_simulated_epoch_id ON simulated_performance_rankings(simulated_epoch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_rankings_node_id ON simulated_performance_rankings(node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_rankings_calculation_method ON simulated_performance_rankings(calculation_method);
|
||||
CREATE INDEX IF NOT EXISTS idx_simulated_performance_rankings_performance_rank ON simulated_performance_rankings(performance_rank);
|
||||
@@ -59,6 +59,11 @@ pub enum RewardingError {
|
||||
#[error("could not obtain the current interval rewarding parameters")]
|
||||
RewardingParamsRetrievalFailure,
|
||||
|
||||
#[error("Database operation failed: {source}")]
|
||||
DatabaseError {
|
||||
source: anyhow::Error,
|
||||
},
|
||||
|
||||
#[error("{0}")]
|
||||
GenericError(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use error::RewardingError;
|
||||
pub(crate) use helpers::RewardedNodeWithParams;
|
||||
use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval};
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use simulation::SimulationConfig;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@@ -32,6 +33,7 @@ mod event_reconciliation;
|
||||
mod helpers;
|
||||
mod rewarded_set_assignment;
|
||||
mod rewarding;
|
||||
pub(crate) mod simulation;
|
||||
mod transition_beginning;
|
||||
|
||||
// naming things is difficult, ok?
|
||||
@@ -42,6 +44,7 @@ pub struct EpochAdvancer {
|
||||
described_cache: SharedCache<DescribedNodes>,
|
||||
status_cache: NodeStatusCache,
|
||||
storage: NymApiStorage,
|
||||
simulation_config: Option<SimulationConfig>,
|
||||
}
|
||||
|
||||
impl EpochAdvancer {
|
||||
@@ -57,6 +60,7 @@ impl EpochAdvancer {
|
||||
status_cache: NodeStatusCache,
|
||||
described_cache: SharedCache<DescribedNodes>,
|
||||
storage: NymApiStorage,
|
||||
simulation_config: Option<SimulationConfig>,
|
||||
) -> Self {
|
||||
EpochAdvancer {
|
||||
nyxd_client,
|
||||
@@ -64,6 +68,7 @@ impl EpochAdvancer {
|
||||
described_cache,
|
||||
status_cache,
|
||||
storage,
|
||||
simulation_config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +153,38 @@ impl EpochAdvancer {
|
||||
}
|
||||
}
|
||||
|
||||
// Run simulation if enabled (before actual rewarding)
|
||||
if let Some(simulation_config) = &self.simulation_config {
|
||||
info!("Running reward simulation for epoch {}", interval.current_epoch_absolute_id());
|
||||
let rewarded_set = match self.nyxd_client.get_rewarded_set_nodes().await {
|
||||
Ok(rewarded_set) => rewarded_set,
|
||||
Err(err) => {
|
||||
warn!("Failed to obtain current rewarded set for simulation: {err}. Falling back to cached version");
|
||||
self.nym_contract_cache
|
||||
.rewarded_set_owned()
|
||||
.await
|
||||
.into_inner()
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(reward_params) = self
|
||||
.nym_contract_cache
|
||||
.interval_reward_params()
|
||||
.await
|
||||
.into_inner()
|
||||
{
|
||||
let _ = self.run_simulation_if_enabled(
|
||||
&rewarded_set,
|
||||
reward_params,
|
||||
interval.current_epoch_absolute_id(),
|
||||
simulation_config.clone(),
|
||||
).await;
|
||||
} else {
|
||||
warn!("Could not obtain reward parameters for simulation");
|
||||
}
|
||||
}
|
||||
|
||||
// Reward all the nodes in the still current, soon to be previous rewarded set
|
||||
info!("Rewarding the current rewarded set...");
|
||||
self.reward_current_rewarded_set(rewards, interval).await?;
|
||||
@@ -158,7 +195,7 @@ impl EpochAdvancer {
|
||||
self.update_rewarded_set_and_advance_epoch(&nym_nodes)
|
||||
.await?;
|
||||
|
||||
info!("Purging old node statuses from the storage...");
|
||||
info!("Purging old data (node statuses and routes) from the storage...");
|
||||
let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp();
|
||||
self.storage.purge_old_statuses(cutoff).await?;
|
||||
|
||||
@@ -297,6 +334,7 @@ impl EpochAdvancer {
|
||||
status_cache: &NodeStatusCache,
|
||||
described_cache: SharedCache<DescribedNodes>,
|
||||
storage: &NymApiStorage,
|
||||
simulation_config: Option<SimulationConfig>,
|
||||
shutdown: &TaskManager,
|
||||
) {
|
||||
let mut rewarded_set_updater = EpochAdvancer::new(
|
||||
@@ -305,6 +343,7 @@ impl EpochAdvancer {
|
||||
status_cache.to_owned(),
|
||||
described_cache,
|
||||
storage.to_owned(),
|
||||
simulation_config,
|
||||
);
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await });
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Performance methodology comparison system
|
||||
//!
|
||||
//! This module provides functionality to compare different performance calculation
|
||||
//! methodologies without affecting actual rewards, enabling analysis of:
|
||||
//! - Old method: 24-hour cache-based performance calculation
|
||||
//! - New method: 1-hour route-based performance calculation
|
||||
//!
|
||||
//! The system focuses on performance metrics and rankings rather than reward amounts,
|
||||
//! as actual rewards are calculated on-chain based on factors not available to the API.
|
||||
|
||||
use crate::epoch_operations::error::RewardingError;
|
||||
use crate::epoch_operations::helpers::RewardedNodeWithParams;
|
||||
use crate::storage::models::{SimulatedNodePerformance, SimulatedPerformanceComparison, SimulatedPerformanceRanking, SimulatedRouteAnalysis};
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::EpochAdvancer;
|
||||
use nym_contracts_common::types::NaiveFloat;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{EpochRewardedSet, NodeId, RewardingParams};
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
/// Configuration for simulation runs
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SimulationConfig {
|
||||
/// Time window in hours for new method calculation (default: 1)
|
||||
pub new_method_time_window_hours: u32,
|
||||
/// Whether to run both old and new methods (default: true)
|
||||
pub run_both_methods: bool,
|
||||
/// Description for this simulation run
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for SimulationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
new_method_time_window_hours: 1,
|
||||
run_both_methods: true,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Main simulation coordinator
|
||||
pub struct SimulationCoordinator<'a> {
|
||||
storage: &'a NymApiStorage,
|
||||
config: SimulationConfig,
|
||||
}
|
||||
|
||||
impl<'a> SimulationCoordinator<'a> {
|
||||
pub fn new(storage: &'a NymApiStorage, config: SimulationConfig) -> Self {
|
||||
Self { storage, config }
|
||||
}
|
||||
|
||||
/// Run a complete simulation comparing old vs new rewarding methods
|
||||
pub async fn run_simulation(
|
||||
&self,
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
current_epoch_id: u32,
|
||||
) -> Result<(), RewardingError> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let end_timestamp = now.unix_timestamp();
|
||||
let start_timestamp = end_timestamp - (24 * 3600); // 24 hours ago for baseline
|
||||
|
||||
info!(
|
||||
"Starting simulation for epoch {} with time window {}h",
|
||||
current_epoch_id, self.config.new_method_time_window_hours
|
||||
);
|
||||
|
||||
// Create simulation epoch record
|
||||
let epoch_db_id = self.storage.manager
|
||||
.create_simulated_reward_epoch(
|
||||
current_epoch_id,
|
||||
"comparison",
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
self.config.description.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
// Run old method simulation (24h cache-based)
|
||||
if self.config.run_both_methods {
|
||||
match self.run_old_method_simulation(
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
end_timestamp,
|
||||
).await {
|
||||
Ok(_) => {
|
||||
info!("Old method simulation completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Old method simulation failed: {}", e);
|
||||
// Continue with new method even if old fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run new method simulation (1h route-based)
|
||||
match self.run_new_method_simulation(
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
end_timestamp,
|
||||
).await {
|
||||
Ok(_) => {
|
||||
info!("New method simulation completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("New method simulation failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Simulation completed for epoch {}", current_epoch_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run simulation using old method (24h cache-based)
|
||||
async fn run_old_method_simulation(
|
||||
&self,
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
end_timestamp: i64,
|
||||
) -> Result<(), RewardingError> {
|
||||
debug!("Running old method simulation (24h cache-based)");
|
||||
|
||||
// Get 24h performance data using existing cache-based method
|
||||
let mixnode_reliabilities = self.storage
|
||||
.get_all_avg_mix_reliability_in_last_24hr(end_timestamp)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
let gateway_reliabilities = self.storage
|
||||
.get_all_avg_gateway_reliability_in_last_24hr(end_timestamp)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
// Convert to performance map
|
||||
let mut performance_map = HashMap::new();
|
||||
|
||||
for mix_reliability in mixnode_reliabilities {
|
||||
performance_map.insert(
|
||||
mix_reliability.mix_id(),
|
||||
Performance::from_percentage_value(mix_reliability.value() as u64).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
for gateway_reliability in gateway_reliabilities {
|
||||
performance_map.insert(
|
||||
gateway_reliability.node_id(),
|
||||
Performance::from_percentage_value(gateway_reliability.value() as u64).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate rewards using old method logic
|
||||
let rewarded_nodes = self.calculate_rewards_for_nodes(
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
&performance_map,
|
||||
);
|
||||
|
||||
// Convert to simulation data structures
|
||||
let node_performance = self.convert_to_simulated_performance(
|
||||
&rewarded_nodes,
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
"old",
|
||||
None, // Old method doesn't have route sample data
|
||||
).await;
|
||||
|
||||
let performance_comparisons = self.convert_to_performance_comparisons(
|
||||
&rewarded_nodes,
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
"old",
|
||||
).await;
|
||||
|
||||
// Create route analysis for old method
|
||||
let route_analysis = SimulatedRouteAnalysis {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_db_id,
|
||||
calculation_method: "old".to_string(),
|
||||
total_routes_analyzed: 0, // Old method doesn't use route data
|
||||
successful_routes: 0,
|
||||
failed_routes: 0,
|
||||
average_route_reliability: None,
|
||||
time_window_hours: 24, // Old method uses 24h
|
||||
analysis_parameters: Some("{\"method\":\"cache_based\",\"data_source\":\"status_cache\"}".to_string()),
|
||||
calculated_at: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
};
|
||||
|
||||
// Store results in database
|
||||
self.storage.manager
|
||||
.insert_simulated_node_performance(&node_performance)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
self.storage.manager
|
||||
.insert_simulated_performance_comparisons(&performance_comparisons)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
// Calculate and store performance rankings
|
||||
let rankings = self.calculate_performance_rankings(&performance_comparisons, epoch_db_id, "old");
|
||||
self.storage.manager
|
||||
.insert_simulated_performance_rankings(&rankings)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
self.storage.manager
|
||||
.insert_simulated_route_analysis(&route_analysis)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run simulation using new method (1h route-based)
|
||||
async fn run_new_method_simulation(
|
||||
&self,
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
end_timestamp: i64,
|
||||
) -> Result<(), RewardingError> {
|
||||
debug!("Running new method simulation ({}h route-based)", self.config.new_method_time_window_hours);
|
||||
|
||||
let time_window_secs = (self.config.new_method_time_window_hours as i64) * 3600;
|
||||
let start_timestamp = end_timestamp - time_window_secs;
|
||||
|
||||
// Get route-based performance data using new method
|
||||
let corrected_reliabilities = self.storage.manager
|
||||
.calculate_corrected_node_reliabilities_for_interval(start_timestamp, end_timestamp)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e })?;
|
||||
|
||||
// Convert to performance map and build route reliability data
|
||||
let mut performance_map = HashMap::new();
|
||||
let mut route_reliability_map = HashMap::new();
|
||||
let mut total_routes = 0u32;
|
||||
let mut successful_routes = 0u32;
|
||||
let mut reliability_sum = 0.0;
|
||||
let mut reliability_count = 0u32;
|
||||
|
||||
for node_reliability in &corrected_reliabilities {
|
||||
let total_samples = node_reliability.pos_samples_in_interval + node_reliability.neg_samples_in_interval;
|
||||
total_routes += total_samples;
|
||||
successful_routes += node_reliability.pos_samples_in_interval;
|
||||
|
||||
if total_samples > 0 {
|
||||
reliability_sum += node_reliability.reliability;
|
||||
reliability_count += 1;
|
||||
}
|
||||
|
||||
performance_map.insert(
|
||||
node_reliability.node_id,
|
||||
Performance::naive_try_from_f64(node_reliability.reliability / 100.0).unwrap_or_default()
|
||||
);
|
||||
|
||||
// Store sample counts for detailed performance records
|
||||
route_reliability_map.insert(
|
||||
node_reliability.node_id,
|
||||
(node_reliability.pos_samples_in_interval, node_reliability.neg_samples_in_interval)
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate rewards using new method logic
|
||||
let rewarded_nodes = self.calculate_rewards_for_nodes(
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
&performance_map,
|
||||
);
|
||||
|
||||
// Convert to simulation data structures
|
||||
let node_performance = self.convert_to_simulated_performance(
|
||||
&rewarded_nodes,
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
"new",
|
||||
Some(&route_reliability_map), // Pass route sample data for new method
|
||||
).await;
|
||||
|
||||
let performance_comparisons = self.convert_to_performance_comparisons(
|
||||
&rewarded_nodes,
|
||||
rewarded_set,
|
||||
reward_params,
|
||||
epoch_db_id,
|
||||
"new",
|
||||
).await;
|
||||
|
||||
// Create route analysis for new method
|
||||
let route_analysis = SimulatedRouteAnalysis {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_db_id,
|
||||
calculation_method: "new".to_string(),
|
||||
total_routes_analyzed: total_routes,
|
||||
successful_routes,
|
||||
failed_routes: total_routes - successful_routes,
|
||||
average_route_reliability: if reliability_count > 0 {
|
||||
Some(reliability_sum / reliability_count as f64)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
time_window_hours: self.config.new_method_time_window_hours,
|
||||
analysis_parameters: Some(format!(
|
||||
"{{\"method\":\"route_based\",\"time_window_hours\":{},\"corrected_routes\":{}}}",
|
||||
self.config.new_method_time_window_hours,
|
||||
corrected_reliabilities.len()
|
||||
)),
|
||||
calculated_at: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
};
|
||||
|
||||
// Store results in database
|
||||
self.storage.manager
|
||||
.insert_simulated_node_performance(&node_performance)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
self.storage.manager
|
||||
.insert_simulated_performance_comparisons(&performance_comparisons)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
// Calculate and store performance rankings
|
||||
let rankings = self.calculate_performance_rankings(&performance_comparisons, epoch_db_id, "new");
|
||||
self.storage.manager
|
||||
.insert_simulated_performance_rankings(&rankings)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
self.storage.manager
|
||||
.insert_simulated_route_analysis(&route_analysis)
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate rewards for nodes using the provided performance data
|
||||
/// This mirrors the logic from helpers.rs but uses simulation performance data
|
||||
fn calculate_rewards_for_nodes(
|
||||
&self,
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
performance_map: &HashMap<NodeId, Performance>,
|
||||
) -> Vec<RewardedNodeWithParams> {
|
||||
let nodes = &rewarded_set.assignment;
|
||||
let active_node_work_factor = reward_params.active_node_work();
|
||||
let standby_node_work_factor = reward_params.standby_node_work();
|
||||
|
||||
let mut rewarded_nodes = Vec::with_capacity(nodes.rewarded_set_size());
|
||||
|
||||
// Process active set mixnodes (layers 1, 2, 3)
|
||||
for &node_id in nodes
|
||||
.layer1
|
||||
.iter()
|
||||
.chain(nodes.layer2.iter())
|
||||
.chain(nodes.layer3.iter())
|
||||
{
|
||||
let performance = performance_map.get(&node_id).copied().unwrap_or_default();
|
||||
rewarded_nodes.push(RewardedNodeWithParams {
|
||||
node_id,
|
||||
params: nym_mixnet_contract_common::reward_params::NodeRewardingParameters {
|
||||
performance,
|
||||
work_factor: active_node_work_factor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Process active set gateways
|
||||
for &node_id in nodes
|
||||
.entry_gateways
|
||||
.iter()
|
||||
.chain(nodes.exit_gateways.iter())
|
||||
{
|
||||
let performance = performance_map.get(&node_id).copied().unwrap_or_default();
|
||||
rewarded_nodes.push(RewardedNodeWithParams {
|
||||
node_id,
|
||||
params: nym_mixnet_contract_common::reward_params::NodeRewardingParameters {
|
||||
performance,
|
||||
work_factor: active_node_work_factor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Process standby nodes
|
||||
for &node_id in &nodes.standby {
|
||||
let performance = performance_map.get(&node_id).copied().unwrap_or_default();
|
||||
rewarded_nodes.push(RewardedNodeWithParams {
|
||||
node_id,
|
||||
params: nym_mixnet_contract_common::reward_params::NodeRewardingParameters {
|
||||
performance,
|
||||
work_factor: standby_node_work_factor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
rewarded_nodes
|
||||
}
|
||||
|
||||
/// Determine node type and work factor from rewarded set position
|
||||
fn determine_node_info(&self, node_id: NodeId, rewarded_set: &EpochRewardedSet, reward_params: RewardingParams) -> (String, f64) {
|
||||
let nodes = &rewarded_set.assignment;
|
||||
|
||||
// Check if node is in active mixnode layers
|
||||
if nodes.layer1.contains(&node_id) || nodes.layer2.contains(&node_id) || nodes.layer3.contains(&node_id) {
|
||||
return ("mixnode".to_string(), reward_params.active_node_work().naive_to_f64());
|
||||
}
|
||||
|
||||
// Check if node is in active gateways
|
||||
if nodes.entry_gateways.contains(&node_id) || nodes.exit_gateways.contains(&node_id) {
|
||||
return ("gateway".to_string(), reward_params.active_node_work().naive_to_f64());
|
||||
}
|
||||
|
||||
// Check if node is in standby (could be mixnode or gateway)
|
||||
if nodes.standby.contains(&node_id) {
|
||||
// Note: We cannot determine if standby nodes are mixnodes or gateways from the
|
||||
// rewarded set data alone. This limitation exists in both old and new calculation
|
||||
// methods and doesn't significantly impact the simulation since:
|
||||
// 1. All standby nodes receive the same work factor regardless of type
|
||||
// 2. The gateway 3-sample rule can't be applied to standby nodes in either method
|
||||
// For consistency, we label all standby nodes as "mixnode" in the simulation data
|
||||
return ("mixnode".to_string(), reward_params.standby_node_work().naive_to_f64());
|
||||
}
|
||||
|
||||
// Default case (shouldn't happen)
|
||||
("unknown".to_string(), 0.0)
|
||||
}
|
||||
|
||||
/// Convert rewarded nodes to simulated performance records
|
||||
async fn convert_to_simulated_performance(
|
||||
&self,
|
||||
rewarded_nodes: &[RewardedNodeWithParams],
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
method: &str,
|
||||
route_reliability_map: Option<&HashMap<NodeId, (u32, u32)>>, // (positive_samples, negative_samples)
|
||||
) -> Vec<SimulatedNodePerformance> {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let mut performance_records = Vec::with_capacity(rewarded_nodes.len());
|
||||
|
||||
// First collect all node IDs for batch identity key lookups
|
||||
let all_node_ids: Vec<NodeId> = rewarded_nodes.iter().map(|node| node.node_id).collect();
|
||||
|
||||
// Batch fetch identity keys for both mixnodes and gateways
|
||||
let mixnode_identities = self.storage
|
||||
.manager
|
||||
.get_mixnode_identity_keys_batch(&all_node_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let gateway_identities = self.storage
|
||||
.manager
|
||||
.get_gateway_identity_keys_batch(&all_node_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for node in rewarded_nodes {
|
||||
let (node_type, _) = self.determine_node_info(node.node_id, rewarded_set, reward_params);
|
||||
|
||||
// Get identity key from our batch results
|
||||
let identity_key = match node_type.as_str() {
|
||||
"mixnode" => mixnode_identities.get(&node.node_id).cloned(),
|
||||
"gateway" => gateway_identities.get(&node.node_id).cloned(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Extract sample counts from route reliability if available
|
||||
let (positive_samples, negative_samples) = route_reliability_map
|
||||
.and_then(|map| map.get(&node.node_id))
|
||||
.copied()
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
performance_records.push(SimulatedNodePerformance {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_db_id,
|
||||
node_id: node.node_id,
|
||||
node_type,
|
||||
identity_key,
|
||||
reliability_score: node.params.performance.naive_to_f64() * 100.0,
|
||||
positive_samples,
|
||||
negative_samples,
|
||||
work_factor: Some(node.params.work_factor.naive_to_f64()),
|
||||
calculation_method: method.to_string(),
|
||||
calculated_at: now,
|
||||
});
|
||||
}
|
||||
|
||||
performance_records
|
||||
}
|
||||
|
||||
/// Convert rewarded nodes to performance comparison records
|
||||
async fn convert_to_performance_comparisons(
|
||||
&self,
|
||||
rewarded_nodes: &[RewardedNodeWithParams],
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
method: &str,
|
||||
) -> Vec<SimulatedPerformanceComparison> {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let mut performance_records = Vec::with_capacity(rewarded_nodes.len());
|
||||
|
||||
for node in rewarded_nodes {
|
||||
let (node_type, _) = self.determine_node_info(node.node_id, rewarded_set, reward_params);
|
||||
|
||||
performance_records.push(SimulatedPerformanceComparison {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_db_id,
|
||||
node_id: node.node_id,
|
||||
node_type,
|
||||
performance_score: node.params.performance.naive_to_f64() * 100.0,
|
||||
work_factor: node.params.work_factor.naive_to_f64(),
|
||||
calculation_method: method.to_string(),
|
||||
positive_samples: None, // Will be populated from node performance data
|
||||
negative_samples: None,
|
||||
route_success_rate: None,
|
||||
calculated_at: now,
|
||||
});
|
||||
}
|
||||
|
||||
performance_records
|
||||
}
|
||||
|
||||
/// Calculate performance rankings for a set of performance comparisons
|
||||
fn calculate_performance_rankings(
|
||||
&self,
|
||||
performance_comparisons: &[SimulatedPerformanceComparison],
|
||||
epoch_db_id: i64,
|
||||
method: &str,
|
||||
) -> Vec<SimulatedPerformanceRanking> {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
let mut rankings = Vec::with_capacity(performance_comparisons.len());
|
||||
|
||||
// Sort by performance score descending
|
||||
let mut sorted_comparisons: Vec<_> = performance_comparisons.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, comp)| (idx, comp.performance_score))
|
||||
.collect();
|
||||
sorted_comparisons.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let total_nodes = sorted_comparisons.len() as f64;
|
||||
|
||||
for (rank, (original_idx, _score)) in sorted_comparisons.iter().enumerate() {
|
||||
let comparison = &performance_comparisons[*original_idx];
|
||||
let percentile = ((total_nodes - rank as f64 - 1.0) / total_nodes) * 100.0;
|
||||
|
||||
rankings.push(SimulatedPerformanceRanking {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_db_id,
|
||||
node_id: comparison.node_id,
|
||||
calculation_method: method.to_string(),
|
||||
performance_rank: (rank + 1) as i64,
|
||||
performance_percentile: percentile,
|
||||
calculated_at: now,
|
||||
});
|
||||
}
|
||||
|
||||
rankings
|
||||
}
|
||||
}
|
||||
|
||||
impl EpochAdvancer {
|
||||
/// Run simulation during epoch operations if simulation mode is enabled
|
||||
pub async fn run_simulation_if_enabled(
|
||||
&self,
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
current_epoch_id: u32,
|
||||
simulation_config: SimulationConfig,
|
||||
) -> Result<(), RewardingError> {
|
||||
let coordinator = SimulationCoordinator::new(&self.storage, simulation_config);
|
||||
|
||||
match coordinator.run_simulation(rewarded_set, reward_params, current_epoch_id).await {
|
||||
Ok(()) => {
|
||||
info!("Simulation completed successfully for epoch {}", current_epoch_id);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Simulation failed for epoch {}: {}", current_epoch_id, e);
|
||||
// Don't fail the entire epoch operation due to simulation failure
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ pub(crate) mod node_describe_cache;
|
||||
pub(crate) mod node_status_api;
|
||||
pub(crate) mod nym_contract_cache;
|
||||
pub(crate) mod nym_nodes;
|
||||
pub(crate) mod simulation_api;
|
||||
mod status;
|
||||
pub(crate) mod support;
|
||||
mod unstable_routes;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Simulation API for reward calculation analysis
|
||||
//!
|
||||
//! This module provides REST endpoints for accessing and analyzing
|
||||
//! simulated reward calculation data comparing old vs new methodologies.
|
||||
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod models;
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! API models for simulation data responses
|
||||
|
||||
use crate::storage::models::{SimulatedNodePerformance, SimulatedPerformanceComparison, SimulatedRewardEpoch, SimulatedRouteAnalysis};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response for listing simulation epochs
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochsResponse {
|
||||
pub epochs: Vec<SimulationEpochSummary>,
|
||||
pub total_count: usize,
|
||||
}
|
||||
|
||||
/// Summary information about a simulation epoch
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochSummary {
|
||||
pub id: i64,
|
||||
pub epoch_id: u32,
|
||||
pub calculation_method: String,
|
||||
pub start_timestamp: i64,
|
||||
pub end_timestamp: i64,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
/// Number of nodes that had performance calculated
|
||||
pub nodes_analyzed: usize,
|
||||
/// Available calculation methods for this epoch
|
||||
pub available_methods: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<SimulatedRewardEpoch> for SimulationEpochSummary {
|
||||
fn from(epoch: SimulatedRewardEpoch) -> Self {
|
||||
Self {
|
||||
id: epoch.id,
|
||||
epoch_id: epoch.epoch_id,
|
||||
calculation_method: epoch.calculation_method,
|
||||
start_timestamp: epoch.start_timestamp,
|
||||
end_timestamp: epoch.end_timestamp,
|
||||
description: epoch.description,
|
||||
created_at: epoch.created_at,
|
||||
nodes_analyzed: 0, // Will be populated by handler
|
||||
available_methods: vec![], // Will be populated by handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed simulation epoch with all data
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochDetails {
|
||||
pub epoch: SimulationEpochSummary,
|
||||
pub node_performance: Vec<NodePerformanceData>,
|
||||
pub performance_comparisons: Vec<PerformanceComparisonData>,
|
||||
pub route_analysis: Vec<RouteAnalysisData>,
|
||||
}
|
||||
|
||||
/// Node performance data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct NodePerformanceData {
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String,
|
||||
pub identity_key: Option<String>,
|
||||
pub reliability_score: f64,
|
||||
pub positive_samples: u32,
|
||||
pub negative_samples: u32,
|
||||
pub work_factor: Option<f64>,
|
||||
pub calculation_method: String,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedNodePerformance> for NodePerformanceData {
|
||||
fn from(perf: SimulatedNodePerformance) -> Self {
|
||||
Self {
|
||||
node_id: perf.node_id,
|
||||
node_type: perf.node_type,
|
||||
identity_key: perf.identity_key,
|
||||
reliability_score: perf.reliability_score,
|
||||
positive_samples: perf.positive_samples,
|
||||
negative_samples: perf.negative_samples,
|
||||
work_factor: perf.work_factor,
|
||||
calculation_method: perf.calculation_method,
|
||||
calculated_at: perf.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance comparison data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct PerformanceComparisonData {
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String,
|
||||
pub performance_score: f64,
|
||||
pub work_factor: f64,
|
||||
pub calculation_method: String,
|
||||
pub positive_samples: Option<i64>,
|
||||
pub negative_samples: Option<i64>,
|
||||
pub route_success_rate: Option<f64>,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedPerformanceComparison> for PerformanceComparisonData {
|
||||
fn from(comparison: SimulatedPerformanceComparison) -> Self {
|
||||
Self {
|
||||
node_id: comparison.node_id,
|
||||
node_type: comparison.node_type,
|
||||
performance_score: comparison.performance_score,
|
||||
work_factor: comparison.work_factor,
|
||||
calculation_method: comparison.calculation_method,
|
||||
positive_samples: comparison.positive_samples,
|
||||
negative_samples: comparison.negative_samples,
|
||||
route_success_rate: comparison.route_success_rate,
|
||||
calculated_at: comparison.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Route analysis data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct RouteAnalysisData {
|
||||
pub calculation_method: String,
|
||||
pub total_routes_analyzed: u32,
|
||||
pub successful_routes: u32,
|
||||
pub failed_routes: u32,
|
||||
pub average_route_reliability: Option<f64>,
|
||||
pub time_window_hours: u32,
|
||||
pub analysis_parameters: Option<String>,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedRouteAnalysis> for RouteAnalysisData {
|
||||
fn from(analysis: SimulatedRouteAnalysis) -> Self {
|
||||
Self {
|
||||
calculation_method: analysis.calculation_method,
|
||||
total_routes_analyzed: analysis.total_routes_analyzed,
|
||||
successful_routes: analysis.successful_routes,
|
||||
failed_routes: analysis.failed_routes,
|
||||
average_route_reliability: analysis.average_route_reliability,
|
||||
time_window_hours: analysis.time_window_hours,
|
||||
analysis_parameters: analysis.analysis_parameters,
|
||||
calculated_at: analysis.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Comparison between old and new methods for a specific epoch
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct MethodComparisonResponse {
|
||||
pub epoch_id: u32,
|
||||
pub simulation_epoch_id: i64,
|
||||
pub node_comparisons: Vec<NodeMethodComparison>,
|
||||
pub summary_statistics: ComparisonSummaryStats,
|
||||
pub route_analysis_comparison: RouteAnalysisComparison,
|
||||
}
|
||||
|
||||
/// Comparison data for a single node between old and new methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct NodeMethodComparison {
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String,
|
||||
pub identity_key: Option<String>,
|
||||
pub old_method: Option<NodePerformanceData>,
|
||||
pub new_method: Option<NodePerformanceData>,
|
||||
pub reliability_difference: Option<f64>, // new - old
|
||||
pub performance_delta_percentage: Option<f64>, // (new - old) / old * 100
|
||||
pub ranking_old_method: Option<i64>,
|
||||
pub ranking_new_method: Option<i64>,
|
||||
pub ranking_delta: Option<i64>, // new ranking - old ranking (negative is improvement)
|
||||
}
|
||||
|
||||
/// Summary statistics comparing old vs new methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct ComparisonSummaryStats {
|
||||
pub total_nodes_compared: usize,
|
||||
pub nodes_improved: usize, // nodes with better performance in new method
|
||||
pub nodes_degraded: usize, // nodes with worse performance in new method
|
||||
pub nodes_unchanged: usize, // nodes with same performance
|
||||
pub average_reliability_old: f64,
|
||||
pub average_reliability_new: f64,
|
||||
pub median_reliability_old: f64,
|
||||
pub median_reliability_new: f64,
|
||||
pub reliability_std_dev_old: f64,
|
||||
pub reliability_std_dev_new: f64,
|
||||
pub max_improvement: f64, // highest positive delta
|
||||
pub max_degradation: f64, // highest negative delta
|
||||
}
|
||||
|
||||
/// Comparison of route analysis between methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct RouteAnalysisComparison {
|
||||
pub old_method: Option<RouteAnalysisData>,
|
||||
pub new_method: Option<RouteAnalysisData>,
|
||||
pub time_window_difference_hours: i32, // new - old
|
||||
pub route_coverage_difference: i32, // new total routes - old total routes
|
||||
pub success_rate_difference: Option<f64>, // new success rate - old success rate
|
||||
}
|
||||
|
||||
/// Export format options
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub enum ExportFormat {
|
||||
#[serde(rename = "json")]
|
||||
Json,
|
||||
#[serde(rename = "csv")]
|
||||
Csv,
|
||||
}
|
||||
|
||||
/// Query parameters for simulation listings
|
||||
#[derive(Deserialize, ToSchema, Debug, utoipa::IntoParams)]
|
||||
pub struct SimulationListQuery {
|
||||
/// Limit number of results (default: 50, max: 1000)
|
||||
pub limit: Option<usize>,
|
||||
/// Offset for pagination (default: 0)
|
||||
pub offset: Option<usize>,
|
||||
}
|
||||
|
||||
/// Query parameters for node-specific performance comparison
|
||||
#[derive(Deserialize, ToSchema, Debug, utoipa::IntoParams)]
|
||||
pub struct NodeComparisonQuery {
|
||||
/// Specific node ID to analyze
|
||||
pub node_id: Option<NodeId>,
|
||||
/// Node type filter (mixnode, gateway)
|
||||
pub node_type: Option<String>,
|
||||
/// Minimum reliability difference threshold for filtering
|
||||
pub min_delta: Option<f64>,
|
||||
/// Maximum reliability difference threshold for filtering
|
||||
pub max_delta: Option<f64>,
|
||||
}
|
||||
|
||||
/// Error response for simulation API
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct SimulationApiError {
|
||||
pub error: String,
|
||||
pub details: Option<String>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl SimulationApiError {
|
||||
pub fn new(error: &str) -> Self {
|
||||
Self {
|
||||
error: error.to_string(),
|
||||
details: None,
|
||||
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_details(error: &str, details: &str) -> Self {
|
||||
Self {
|
||||
error: error.to_string(),
|
||||
details: Some(details.to_string()),
|
||||
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::ecash::dkg::controller::keys::{
|
||||
};
|
||||
use crate::ecash::dkg::controller::DkgController;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::epoch_operations::EpochAdvancer;
|
||||
use crate::epoch_operations::{EpochAdvancer, simulation::SimulationConfig};
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::handlers::unstable;
|
||||
@@ -61,10 +61,22 @@ pub(crate) struct Args {
|
||||
long,
|
||||
requires = "enable_monitor",
|
||||
requires = "mnemonic",
|
||||
conflicts_with = "simulate_rewarding",
|
||||
env = "NYMAPI_ENABLE_REWARDING_ARG"
|
||||
)]
|
||||
pub(crate) enable_rewarding: Option<bool>,
|
||||
|
||||
/// Enable simulated rewarding mode to compare old vs new reward calculations
|
||||
/// Requires monitoring but does NOT require mnemonic (no blockchain transactions)
|
||||
/// default: None - config value will be used instead
|
||||
#[clap(
|
||||
long,
|
||||
requires = "enable_monitor",
|
||||
conflicts_with = "enable_rewarding",
|
||||
env = "NYMAPI_SIMULATE_REWARDING_ARG"
|
||||
)]
|
||||
pub(crate) simulate_rewarding: Option<bool>,
|
||||
|
||||
/// Endpoint to nyxd instance used for contract information.
|
||||
/// default: None - config value will be used instead
|
||||
#[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")]
|
||||
@@ -276,15 +288,31 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
|
||||
|
||||
HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager);
|
||||
|
||||
// start 'rewarding' if its enabled
|
||||
if config.rewarding.enabled {
|
||||
epoch_operations::ensure_rewarding_permission(&nyxd_client).await?;
|
||||
// start 'rewarding' or 'simulation' if enabled
|
||||
if config.rewarding.enabled || config.rewarding.simulation_mode {
|
||||
// Only check rewarding permission for real rewarding, not simulation
|
||||
if config.rewarding.enabled && !config.rewarding.simulation_mode {
|
||||
epoch_operations::ensure_rewarding_permission(&nyxd_client).await?;
|
||||
}
|
||||
|
||||
// Create simulation config if simulation mode is enabled
|
||||
let simulation_config = if config.rewarding.simulation_mode {
|
||||
Some(SimulationConfig {
|
||||
new_method_time_window_hours: config.rewarding.debug.simulation_new_method_time_window_hours,
|
||||
run_both_methods: config.rewarding.debug.simulation_run_both_methods,
|
||||
description: Some("Simulation run at epoch advancement".to_string()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
EpochAdvancer::start(
|
||||
nyxd_client,
|
||||
&nym_contract_cache_state,
|
||||
&node_status_cache_state,
|
||||
described_nodes_cache.clone(),
|
||||
&storage,
|
||||
simulation_config,
|
||||
&task_manager,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,14 +136,30 @@ impl Config {
|
||||
pub fn validate(&self) -> anyhow::Result<()> {
|
||||
let can_sign = self.base.mnemonic.is_some();
|
||||
|
||||
if !can_sign && self.rewarding.enabled {
|
||||
// Real rewarding requires mnemonic, but simulation mode does not
|
||||
if !can_sign && self.rewarding.enabled && !self.rewarding.simulation_mode {
|
||||
bail!("can't enable rewarding without providing a mnemonic")
|
||||
}
|
||||
|
||||
// Simulation mode and real rewarding are mutually exclusive
|
||||
if self.rewarding.enabled && self.rewarding.simulation_mode {
|
||||
bail!("cannot enable both real rewarding and simulation mode simultaneously")
|
||||
}
|
||||
|
||||
if !can_sign && self.ecash_signer.enabled {
|
||||
bail!("can't enable coconut signer without providing a mnemonic")
|
||||
}
|
||||
|
||||
// Validate simulation-specific settings
|
||||
if self.rewarding.simulation_mode {
|
||||
if self.rewarding.debug.simulation_new_method_time_window_hours == 0 {
|
||||
bail!("simulation_new_method_time_window_hours must be greater than 0")
|
||||
}
|
||||
if self.rewarding.debug.simulation_new_method_time_window_hours > 24 {
|
||||
bail!("simulation_new_method_time_window_hours should not exceed 24 hours")
|
||||
}
|
||||
}
|
||||
|
||||
self.ecash_signer.validate()?;
|
||||
|
||||
Ok(())
|
||||
@@ -158,6 +174,9 @@ impl Config {
|
||||
if let Some(enable_rewarding) = args.enable_rewarding {
|
||||
self.rewarding.enabled = enable_rewarding;
|
||||
}
|
||||
if let Some(simulate_rewarding) = args.simulate_rewarding {
|
||||
self.rewarding.simulation_mode = simulate_rewarding;
|
||||
}
|
||||
if let Some(nyxd_upstream) = args.nyxd_validator {
|
||||
self.base.local_validator = nyxd_upstream;
|
||||
}
|
||||
@@ -499,6 +518,9 @@ pub struct Rewarding {
|
||||
/// Specifies whether rewarding service is enabled in this process.
|
||||
pub enabled: bool,
|
||||
|
||||
/// Specifies whether to run in simulation mode (compare old vs new calculations without blockchain transactions)
|
||||
pub simulation_mode: bool,
|
||||
|
||||
// this should really be a thing too...
|
||||
// pub paths: RewardingPathfinder,
|
||||
#[serde(default)]
|
||||
@@ -510,6 +532,7 @@ impl Default for Rewarding {
|
||||
fn default() -> Self {
|
||||
Rewarding {
|
||||
enabled: false,
|
||||
simulation_mode: false,
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -522,12 +545,21 @@ pub struct RewardingDebug {
|
||||
/// distribute rewards for given interval.
|
||||
/// Note, only values in range 0-100 are valid
|
||||
pub minimum_interval_monitor_threshold: u8,
|
||||
|
||||
/// Time window in hours for new route-based performance calculation (default: 1 hour)
|
||||
/// Old method always uses 24 hours for comparison
|
||||
pub simulation_new_method_time_window_hours: u32,
|
||||
|
||||
/// Whether to run both old and new calculations in simulation mode
|
||||
pub simulation_run_both_methods: bool,
|
||||
}
|
||||
|
||||
impl Default for RewardingDebug {
|
||||
fn default() -> Self {
|
||||
RewardingDebug {
|
||||
minimum_interval_monitor_threshold: DEFAULT_MONITOR_THRESHOLD,
|
||||
simulation_new_method_time_window_hours: 1, // New method uses 1 hour
|
||||
simulation_run_both_methods: true, // Default to running both for comparison
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ pub(crate) struct OverrideConfig {
|
||||
/// Specifies whether network rewarding is enabled on this API
|
||||
pub(crate) enable_rewarding: Option<bool>,
|
||||
|
||||
/// Specifies whether simulated rewarding is enabled on this API
|
||||
pub(crate) simulate_rewarding: Option<bool>,
|
||||
|
||||
/// Endpoint to nyxd instance used for contract information.
|
||||
pub(crate) nyxd_validator: Option<url::Url>,
|
||||
|
||||
@@ -40,6 +43,7 @@ impl From<init::Args> for OverrideConfig {
|
||||
OverrideConfig {
|
||||
enable_monitor: Some(args.enable_monitor),
|
||||
enable_rewarding: Some(args.enable_rewarding),
|
||||
simulate_rewarding: None, // Not available during initialization
|
||||
nyxd_validator: args.nyxd_validator,
|
||||
mnemonic: args.mnemonic,
|
||||
enable_zk_nym: Some(args.enable_zk_nym),
|
||||
@@ -56,6 +60,7 @@ impl From<run::Args> for OverrideConfig {
|
||||
OverrideConfig {
|
||||
enable_monitor: args.enable_monitor,
|
||||
enable_rewarding: args.enable_rewarding,
|
||||
simulate_rewarding: args.simulate_rewarding,
|
||||
nyxd_validator: args.nyxd_validator,
|
||||
mnemonic: args.mnemonic,
|
||||
enable_zk_nym: args.enable_zk_nym,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::node_status_api::handlers::status_routes;
|
||||
use crate::nym_contract_cache::handlers::nym_contract_cache_routes;
|
||||
use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes;
|
||||
use crate::nym_nodes::handlers::nym_node_routes;
|
||||
use crate::simulation_api::handlers::simulation_routes;
|
||||
use crate::status;
|
||||
use crate::support::http::openapi::ApiDoc;
|
||||
use crate::support::http::state::AppState;
|
||||
@@ -64,6 +65,7 @@ impl RouterBuilder {
|
||||
.nest("/api-status", status::handlers::api_status_routes())
|
||||
.nest("/nym-nodes", nym_node_routes())
|
||||
.nest("/ecash", ecash_routes())
|
||||
.nest("/simulation", simulation_routes())
|
||||
.nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::support::storage::models::{
|
||||
use crate::support::storage::DbIdCache;
|
||||
use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId};
|
||||
use nym_types::monitoring::{NodeResult, RouteResult};
|
||||
use sqlx::FromRow;
|
||||
use sqlx::{FromRow, Row};
|
||||
use std::collections::HashMap;
|
||||
use time::{Date, OffsetDateTime};
|
||||
use tracing::info;
|
||||
@@ -101,16 +101,21 @@ impl StorageManager {
|
||||
.await
|
||||
.map_err(|_e| sqlx::Error::PoolClosed)?; // Example: map anyhow::Error to sqlx::Error; adjust as needed
|
||||
|
||||
// Collect all node IDs for batch classification
|
||||
let node_ids: Vec<NodeId> = corrected_reliabilities
|
||||
.iter()
|
||||
.map(|node| node.node_id)
|
||||
.collect();
|
||||
|
||||
// Batch classify nodes to determine which are mixnodes
|
||||
let (mixnode_ids, _) = self.classify_nodes_batch(&node_ids).await?;
|
||||
let mixnode_set: std::collections::HashSet<NodeId> = mixnode_ids.into_iter().collect();
|
||||
|
||||
let mut avg_mix_reliabilities = Vec::new();
|
||||
|
||||
for corrected_node_info in corrected_reliabilities {
|
||||
// Check if this node_id is a mixnode by attempting to fetch its identity key as a mixnode.
|
||||
// This relies on get_mixnode_identity_key returning Some for mixnodes and None (or error) for non-mixnodes.
|
||||
if self
|
||||
.get_mixnode_identity_key(corrected_node_info.node_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
// Check if this node_id is a mixnode using our batch result
|
||||
if mixnode_set.contains(&corrected_node_info.node_id) {
|
||||
avg_mix_reliabilities.push(AvgMixnodeReliability {
|
||||
mix_id: corrected_node_info.node_id,
|
||||
value: Some(corrected_node_info.reliability as f32),
|
||||
@@ -131,15 +136,21 @@ impl StorageManager {
|
||||
.await
|
||||
.map_err(|_e| sqlx::Error::PoolClosed)?; // Example: map anyhow::Error to sqlx::Error; adjust as needed
|
||||
|
||||
// Collect all node IDs for batch classification
|
||||
let node_ids: Vec<NodeId> = corrected_reliabilities
|
||||
.iter()
|
||||
.map(|node| node.node_id)
|
||||
.collect();
|
||||
|
||||
// Batch classify nodes to determine which are gateways
|
||||
let (_, gateway_ids) = self.classify_nodes_batch(&node_ids).await?;
|
||||
let gateway_set: std::collections::HashSet<NodeId> = gateway_ids.into_iter().collect();
|
||||
|
||||
let mut avg_gateway_reliabilities = Vec::new();
|
||||
|
||||
for corrected_node_info in corrected_reliabilities {
|
||||
// Check if this node_id is a gateway.
|
||||
if self
|
||||
.get_gateway_identity_key(corrected_node_info.node_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
// Check if this node_id is a gateway using our batch result
|
||||
if gateway_set.contains(&corrected_node_info.node_id) {
|
||||
let total_samples = corrected_node_info.pos_samples_in_interval
|
||||
+ corrected_node_info.neg_samples_in_interval;
|
||||
let reliability_value = if total_samples <= 3 {
|
||||
@@ -232,6 +243,90 @@ impl StorageManager {
|
||||
Ok(identity_key)
|
||||
}
|
||||
|
||||
/// Batch version of get_gateway_identity_key for improved performance.
|
||||
/// Returns a HashMap mapping node_id to identity for all found gateways.
|
||||
pub(crate) async fn get_gateway_identity_keys_batch(
|
||||
&self,
|
||||
node_ids: &[NodeId],
|
||||
) -> Result<std::collections::HashMap<NodeId, IdentityKey>, sqlx::Error> {
|
||||
if node_ids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
// Create placeholders for the IN clause
|
||||
let placeholders = node_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
let query = format!(
|
||||
"SELECT node_id, identity FROM gateway_details WHERE node_id IN ({})",
|
||||
placeholders
|
||||
);
|
||||
|
||||
let mut query_builder = sqlx::query(&query);
|
||||
for node_id in node_ids {
|
||||
query_builder = query_builder.bind(node_id);
|
||||
}
|
||||
|
||||
let rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
let node_id: NodeId = row.try_get("node_id")?;
|
||||
let identity: Option<String> = row.try_get("identity")?;
|
||||
if let Some(key) = identity {
|
||||
result.insert(node_id, key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Batch method to classify nodes as mixnodes or gateways.
|
||||
/// Returns (mixnode_ids, gateway_ids) as separate vectors for easier processing.
|
||||
pub(crate) async fn classify_nodes_batch(
|
||||
&self,
|
||||
node_ids: &[NodeId],
|
||||
) -> Result<(Vec<NodeId>, Vec<NodeId>), sqlx::Error> {
|
||||
if node_ids.is_empty() {
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
|
||||
// Query both tables to determine which nodes are mixnodes vs gateways
|
||||
let placeholders = node_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
|
||||
// First get all mixnode IDs
|
||||
let mixnode_query = format!(
|
||||
"SELECT mix_id FROM mixnode_details WHERE mix_id IN ({})",
|
||||
placeholders
|
||||
);
|
||||
let mut query_builder = sqlx::query(&mixnode_query);
|
||||
for node_id in node_ids {
|
||||
query_builder = query_builder.bind(node_id);
|
||||
}
|
||||
let mixnode_rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let mixnode_ids: Vec<NodeId> = mixnode_rows
|
||||
.into_iter()
|
||||
.map(|row| row.try_get("mix_id"))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Then get all gateway IDs
|
||||
let gateway_query = format!(
|
||||
"SELECT node_id FROM gateway_details WHERE node_id IN ({})",
|
||||
placeholders
|
||||
);
|
||||
let mut query_builder = sqlx::query(&gateway_query);
|
||||
for node_id in node_ids {
|
||||
query_builder = query_builder.bind(node_id);
|
||||
}
|
||||
let gateway_rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let gateway_ids: Vec<NodeId> = gateway_rows
|
||||
.into_iter()
|
||||
.map(|row| row.try_get("node_id"))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok((mixnode_ids, gateway_ids))
|
||||
}
|
||||
|
||||
/// Tries to obtain identity value of given mixnode given its mix_id
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -252,6 +347,42 @@ impl StorageManager {
|
||||
Ok(identity_key)
|
||||
}
|
||||
|
||||
/// Batch version of get_mixnode_identity_key for improved performance.
|
||||
/// Returns a HashMap mapping node_id to identity_key for all found mixnodes.
|
||||
pub(crate) async fn get_mixnode_identity_keys_batch(
|
||||
&self,
|
||||
mix_ids: &[NodeId],
|
||||
) -> Result<std::collections::HashMap<NodeId, IdentityKey>, sqlx::Error> {
|
||||
if mix_ids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
// Create placeholders for the IN clause
|
||||
let placeholders = mix_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
let query = format!(
|
||||
"SELECT mix_id, identity_key FROM mixnode_details WHERE mix_id IN ({})",
|
||||
placeholders
|
||||
);
|
||||
|
||||
let mut query_builder = sqlx::query(&query);
|
||||
for mix_id in mix_ids {
|
||||
query_builder = query_builder.bind(mix_id);
|
||||
}
|
||||
|
||||
let rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
let mix_id: NodeId = row.try_get("mix_id")?;
|
||||
let identity_key: Option<String> = row.try_get("identity_key")?;
|
||||
if let Some(key) = identity_key {
|
||||
result.insert(mix_id, key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Gets all reliability statuses for mixnode with particular identity that were inserted
|
||||
/// into the database after the specified unix timestamp.
|
||||
///
|
||||
@@ -1067,6 +1198,22 @@ impl StorageManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all route monitoring results that are older than the
|
||||
/// provided timestamp. This method is indirectly called at every reward cycle.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timestamp`: timestamp specifying the purge cutoff.
|
||||
pub(super) async fn purge_old_routes(
|
||||
&self,
|
||||
timestamp: i64,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM routes WHERE timestamp < ?", timestamp)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns public key, owner and id of all mixnodes that have had any statuses submitted
|
||||
/// within the provided time interval.
|
||||
///
|
||||
@@ -1528,69 +1675,70 @@ impl StorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod v3_migration {
|
||||
use crate::support::storage::manager::StorageManager;
|
||||
use crate::support::storage::models::GatewayDetailsBeforeMigration;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use crate::storage::models::{
|
||||
SimulatedNodePerformance, SimulatedPerformanceComparison, SimulatedPerformanceRanking,
|
||||
SimulatedRewardEpoch, SimulatedRouteAnalysis,
|
||||
};
|
||||
use crate::support::storage::models::GatewayDetailsBeforeMigration;
|
||||
|
||||
impl StorageManager {
|
||||
pub(crate) async fn check_v3_migration(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|result| result.exists == Some(1))
|
||||
}
|
||||
impl StorageManager {
|
||||
pub(crate) async fn check_v3_migration(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|result| result.exists == Some(1))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO v3_migration_info(id) VALUES (0)")
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all_known_gateways(
|
||||
&self,
|
||||
) -> Result<Vec<GatewayDetailsBeforeMigration>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM gateway_details")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_gateway_node_id(
|
||||
&self,
|
||||
identity: &str,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE gateway_details SET node_id = ? WHERE identity = ?",
|
||||
node_id,
|
||||
identity
|
||||
)
|
||||
pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO v3_migration_info(id) VALUES (0)")
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
pub(crate) async fn get_all_known_gateways(
|
||||
&self,
|
||||
) -> Result<Vec<GatewayDetailsBeforeMigration>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM gateway_details")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_gateway_node_id(
|
||||
&self,
|
||||
identity: &str,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE gateway_details SET node_id = ? WHERE identity = ?",
|
||||
node_id,
|
||||
identity
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM gateway_historical_uptime WHERE gateway_details_id = ?;
|
||||
DELETE FROM gateway_status WHERE gateway_details_id = ?;
|
||||
DELETE FROM testing_route WHERE gateway_id = ?;
|
||||
DELETE FROM gateway_details WHERE id = ?;
|
||||
"#,
|
||||
db_id,
|
||||
db_id,
|
||||
db_id,
|
||||
db_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
db_id,
|
||||
db_id,
|
||||
db_id,
|
||||
db_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn make_node_id_not_null(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
pub(crate) async fn make_node_id_not_null(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE gateway_details_temp
|
||||
(
|
||||
@@ -1606,7 +1754,533 @@ pub(crate) mod v3_migration {
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Simulated Rewarding System CRUD Methods
|
||||
|
||||
/// Creates a new simulated reward epoch and returns its ID
|
||||
pub(crate) async fn create_simulated_reward_epoch(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
calculation_method: &str,
|
||||
start_timestamp: i64,
|
||||
end_timestamp: i64,
|
||||
description: Option<&str>,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO simulated_reward_epochs
|
||||
(epoch_id, calculation_method, start_timestamp, end_timestamp, description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"#,
|
||||
epoch_id,
|
||||
calculation_method,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
description,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Inserts simulated node performance data
|
||||
pub(crate) async fn insert_simulated_node_performance(
|
||||
&self,
|
||||
performance_data: &[SimulatedNodePerformance],
|
||||
) -> Result<(), sqlx::Error> {
|
||||
if performance_data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Use bulk insert with sqlx::QueryBuilder for better performance
|
||||
let mut query_builder = sqlx::QueryBuilder::new(
|
||||
"INSERT INTO simulated_node_performance (simulated_epoch_id, node_id, node_type, identity_key, reliability_score, positive_samples, negative_samples, work_factor, calculation_method) "
|
||||
);
|
||||
|
||||
query_builder.push_values(performance_data, |mut b, performance| {
|
||||
b.push_bind(performance.simulated_epoch_id)
|
||||
.push_bind(performance.node_id)
|
||||
.push_bind(&performance.node_type)
|
||||
.push_bind(&performance.identity_key)
|
||||
.push_bind(performance.reliability_score)
|
||||
.push_bind(performance.positive_samples)
|
||||
.push_bind(performance.negative_samples)
|
||||
.push_bind(performance.work_factor)
|
||||
.push_bind(&performance.calculation_method);
|
||||
});
|
||||
|
||||
query_builder.build().execute(&self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts performance comparison data for simulation analysis
|
||||
pub(crate) async fn insert_simulated_performance_comparisons(
|
||||
&self,
|
||||
performance_data: &[SimulatedPerformanceComparison],
|
||||
) -> Result<(), sqlx::Error> {
|
||||
if performance_data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Use bulk insert with sqlx::QueryBuilder for better performance
|
||||
let mut query_builder = sqlx::QueryBuilder::new(
|
||||
"INSERT INTO simulated_performance_comparisons (simulated_epoch_id, node_id, node_type, performance_score, work_factor, calculation_method, positive_samples, negative_samples, route_success_rate) "
|
||||
);
|
||||
|
||||
query_builder.push_values(performance_data, |mut b, comparison| {
|
||||
b.push_bind(comparison.simulated_epoch_id)
|
||||
.push_bind(comparison.node_id)
|
||||
.push_bind(&comparison.node_type)
|
||||
.push_bind(comparison.performance_score)
|
||||
.push_bind(comparison.work_factor)
|
||||
.push_bind(&comparison.calculation_method)
|
||||
.push_bind(comparison.positive_samples)
|
||||
.push_bind(comparison.negative_samples)
|
||||
.push_bind(comparison.route_success_rate);
|
||||
});
|
||||
|
||||
query_builder.build().execute(&self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts performance rankings for simulation
|
||||
pub(crate) async fn insert_simulated_performance_rankings(
|
||||
&self,
|
||||
rankings: &[SimulatedPerformanceRanking],
|
||||
) -> Result<(), sqlx::Error> {
|
||||
if rankings.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Use bulk insert with sqlx::QueryBuilder for better performance
|
||||
let mut query_builder = sqlx::QueryBuilder::new(
|
||||
"INSERT INTO simulated_performance_rankings (simulated_epoch_id, node_id, calculation_method, performance_rank, performance_percentile) "
|
||||
);
|
||||
|
||||
query_builder.push_values(rankings, |mut b, ranking| {
|
||||
b.push_bind(ranking.simulated_epoch_id)
|
||||
.push_bind(ranking.node_id)
|
||||
.push_bind(&ranking.calculation_method)
|
||||
.push_bind(ranking.performance_rank)
|
||||
.push_bind(ranking.performance_percentile);
|
||||
});
|
||||
|
||||
query_builder.build().execute(&self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts route analysis metadata for a simulation
|
||||
pub(crate) async fn insert_simulated_route_analysis(
|
||||
&self,
|
||||
analysis: &SimulatedRouteAnalysis,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO simulated_route_analysis
|
||||
(simulated_epoch_id, calculation_method, total_routes_analyzed, successful_routes,
|
||||
failed_routes, average_route_reliability, time_window_hours, analysis_parameters)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
analysis.simulated_epoch_id,
|
||||
analysis.calculation_method,
|
||||
analysis.total_routes_analyzed,
|
||||
analysis.successful_routes,
|
||||
analysis.failed_routes,
|
||||
analysis.average_route_reliability,
|
||||
analysis.time_window_hours,
|
||||
analysis.analysis_parameters,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves node performance data for a specific simulation
|
||||
pub(crate) async fn get_simulated_node_performance(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
calculation_method: Option<&str>,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
match calculation_method {
|
||||
Some(method) => {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!", node_id as "node_id!: NodeId",
|
||||
node_type as "node_type!", identity_key, reliability_score as "reliability_score!",
|
||||
positive_samples as "positive_samples!: u32", negative_samples as "negative_samples!: u32",
|
||||
work_factor, calculation_method as "calculation_method!", calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance
|
||||
WHERE simulated_epoch_id = ? AND calculation_method = ?
|
||||
ORDER BY node_id
|
||||
"#,
|
||||
simulated_epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!", node_id as "node_id!: NodeId",
|
||||
node_type as "node_type!", identity_key, reliability_score as "reliability_score!",
|
||||
positive_samples as "positive_samples!: u32", negative_samples as "negative_samples!: u32",
|
||||
work_factor, calculation_method as "calculation_method!", calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance
|
||||
WHERE simulated_epoch_id = ?
|
||||
ORDER BY calculation_method, node_id
|
||||
"#,
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves performance comparison data for a specific simulation
|
||||
pub(crate) async fn get_simulated_performance_comparisons(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
calculation_method: Option<&str>,
|
||||
) -> Result<Vec<SimulatedPerformanceComparison>, sqlx::Error> {
|
||||
match calculation_method {
|
||||
Some(method) => {
|
||||
sqlx::query_as!(
|
||||
SimulatedPerformanceComparison,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!", node_id as "node_id!: NodeId",
|
||||
node_type as "node_type!", performance_score as "performance_score!",
|
||||
work_factor as "work_factor!", calculation_method as "calculation_method!",
|
||||
positive_samples as "positive_samples?", negative_samples as "negative_samples?",
|
||||
route_success_rate as "route_success_rate?", calculated_at as "calculated_at!"
|
||||
FROM simulated_performance_comparisons
|
||||
WHERE simulated_epoch_id = ? AND calculation_method = ?
|
||||
ORDER BY node_id
|
||||
"#,
|
||||
simulated_epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
SimulatedPerformanceComparison,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!", node_id as "node_id!: NodeId",
|
||||
node_type as "node_type!", performance_score as "performance_score!",
|
||||
work_factor as "work_factor!", calculation_method as "calculation_method!",
|
||||
positive_samples as "positive_samples?", negative_samples as "negative_samples?",
|
||||
route_success_rate as "route_success_rate?", calculated_at as "calculated_at!"
|
||||
FROM simulated_performance_comparisons
|
||||
WHERE simulated_epoch_id = ?
|
||||
ORDER BY calculation_method, node_id
|
||||
"#,
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Count simulated node performance records for a specific epoch
|
||||
pub(crate) async fn count_simulated_node_performance_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<usize, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM simulated_node_performance WHERE simulated_epoch_id = ?",
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.count as usize)
|
||||
}
|
||||
|
||||
/// Get available calculation methods for a specific epoch
|
||||
pub(crate) async fn get_available_calculation_methods_for_epoch(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
) -> Result<Vec<String>, sqlx::Error> {
|
||||
let methods = sqlx::query!(
|
||||
"SELECT DISTINCT calculation_method FROM simulated_reward_epochs WHERE epoch_id = ?",
|
||||
epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(methods.into_iter().map(|m| m.calculation_method).collect())
|
||||
}
|
||||
|
||||
/// Batch version of count_simulated_node_performance_for_epoch for improved performance.
|
||||
/// Returns a HashMap mapping epoch_id to node count for all requested epochs.
|
||||
pub(crate) async fn count_simulated_node_performance_for_epochs_batch(
|
||||
&self,
|
||||
simulated_epoch_ids: &[i64],
|
||||
) -> Result<std::collections::HashMap<i64, usize>, sqlx::Error> {
|
||||
|
||||
if simulated_epoch_ids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let placeholders = simulated_epoch_ids
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let query = format!(
|
||||
"SELECT simulated_epoch_id, COUNT(*) as count FROM simulated_node_performance WHERE simulated_epoch_id IN ({}) GROUP BY simulated_epoch_id",
|
||||
placeholders
|
||||
);
|
||||
|
||||
let mut query_builder = sqlx::query(&query);
|
||||
for epoch_id in simulated_epoch_ids {
|
||||
query_builder = query_builder.bind(epoch_id);
|
||||
}
|
||||
|
||||
let rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
let epoch_id: i64 = row.try_get("simulated_epoch_id")?;
|
||||
let count: i64 = row.try_get("count")?;
|
||||
result.insert(epoch_id, count as usize);
|
||||
}
|
||||
|
||||
// Initialize missing epochs with 0 count
|
||||
for &epoch_id in simulated_epoch_ids {
|
||||
result.entry(epoch_id).or_insert(0);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Batch version of get_available_calculation_methods_for_epoch for improved performance.
|
||||
/// Returns a HashMap mapping epoch_id to list of calculation methods for all requested epochs.
|
||||
pub(crate) async fn get_available_calculation_methods_for_epochs_batch(
|
||||
&self,
|
||||
epoch_ids: &[u32],
|
||||
) -> Result<std::collections::HashMap<u32, Vec<String>>, sqlx::Error> {
|
||||
if epoch_ids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let placeholders = epoch_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
let query = format!(
|
||||
"SELECT epoch_id, calculation_method FROM simulated_reward_epochs WHERE epoch_id IN ({})",
|
||||
placeholders
|
||||
);
|
||||
|
||||
let mut query_builder = sqlx::query(&query);
|
||||
for epoch_id in epoch_ids {
|
||||
query_builder = query_builder.bind(epoch_id);
|
||||
}
|
||||
|
||||
let rows = query_builder.fetch_all(&self.connection_pool).await?;
|
||||
|
||||
let mut result: std::collections::HashMap<u32, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
let epoch_id: u32 = row.try_get("epoch_id")?;
|
||||
let method: String = row.try_get("calculation_method")?;
|
||||
result.entry(epoch_id).or_default().push(method);
|
||||
}
|
||||
|
||||
// Initialize missing epochs with empty Vec
|
||||
for &epoch_id in epoch_ids {
|
||||
result.entry(epoch_id).or_default();
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get simulated reward epoch by ID
|
||||
pub(crate) async fn get_simulated_reward_epoch(
|
||||
&self,
|
||||
id: i64,
|
||||
) -> Result<Option<SimulatedRewardEpoch>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRewardEpoch,
|
||||
r#"
|
||||
SELECT id as "id!", epoch_id as "epoch_id!: u32", calculation_method as "calculation_method!",
|
||||
start_timestamp as "start_timestamp!", end_timestamp as "end_timestamp!",
|
||||
description, created_at as "created_at!"
|
||||
FROM simulated_reward_epochs
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated node performance for a specific epoch
|
||||
pub(crate) async fn get_simulated_node_performance_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
self.get_simulated_node_performance(simulated_epoch_id, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get performance comparisons for a specific epoch
|
||||
pub(crate) async fn get_simulated_performance_comparisons_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedPerformanceComparison>, sqlx::Error> {
|
||||
self.get_simulated_performance_comparisons(simulated_epoch_id, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get performance rankings for a specific epoch and method
|
||||
pub(crate) async fn get_simulated_performance_rankings(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
calculation_method: Option<&str>,
|
||||
) -> Result<Vec<SimulatedPerformanceRanking>, sqlx::Error> {
|
||||
match calculation_method {
|
||||
Some(method) => {
|
||||
sqlx::query_as!(
|
||||
SimulatedPerformanceRanking,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!",
|
||||
node_id as "node_id!: NodeId", calculation_method as "calculation_method!",
|
||||
performance_rank as "performance_rank!", performance_percentile as "performance_percentile!",
|
||||
calculated_at as "calculated_at!"
|
||||
FROM simulated_performance_rankings
|
||||
WHERE simulated_epoch_id = ? AND calculation_method = ?
|
||||
ORDER BY performance_rank
|
||||
"#,
|
||||
simulated_epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
SimulatedPerformanceRanking,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!",
|
||||
node_id as "node_id!: NodeId", calculation_method as "calculation_method!",
|
||||
performance_rank as "performance_rank!", performance_percentile as "performance_percentile!",
|
||||
calculated_at as "calculated_at!"
|
||||
FROM simulated_performance_rankings
|
||||
WHERE simulated_epoch_id = ?
|
||||
ORDER BY calculation_method, performance_rank
|
||||
"#,
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get simulated route analysis for a specific epoch
|
||||
pub(crate) async fn get_simulated_route_analysis_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedRouteAnalysis>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRouteAnalysis,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!",
|
||||
calculation_method as "calculation_method!", total_routes_analyzed as "total_routes_analyzed!: u32",
|
||||
successful_routes as "successful_routes!: u32", failed_routes as "failed_routes!: u32",
|
||||
average_route_reliability, time_window_hours as "time_window_hours!: u32",
|
||||
analysis_parameters, calculated_at as "calculated_at!"
|
||||
FROM simulated_route_analysis
|
||||
WHERE simulated_epoch_id = ?
|
||||
ORDER BY calculation_method
|
||||
"#,
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated node performance by method
|
||||
pub(crate) async fn get_simulated_node_performance_by_method(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
method: &str,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT snp.id as "id!", snp.simulated_epoch_id as "simulated_epoch_id!",
|
||||
snp.node_id as "node_id!: NodeId", snp.node_type as "node_type!",
|
||||
snp.identity_key, snp.reliability_score as "reliability_score!",
|
||||
snp.positive_samples as "positive_samples!: u32", snp.negative_samples as "negative_samples!: u32",
|
||||
snp.work_factor, snp.calculation_method as "calculation_method!", snp.calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance snp
|
||||
JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id
|
||||
WHERE sre.epoch_id = ? AND snp.calculation_method = ?
|
||||
ORDER BY snp.node_id
|
||||
"#,
|
||||
epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated route analysis by method
|
||||
pub(crate) async fn get_simulated_route_analysis_by_method(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
method: &str,
|
||||
) -> Result<Option<SimulatedRouteAnalysis>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRouteAnalysis,
|
||||
r#"
|
||||
SELECT sra.id as "id!", sra.simulated_epoch_id as "simulated_epoch_id!",
|
||||
sra.calculation_method as "calculation_method!", sra.total_routes_analyzed as "total_routes_analyzed!: u32",
|
||||
sra.successful_routes as "successful_routes!: u32", sra.failed_routes as "failed_routes!: u32",
|
||||
sra.average_route_reliability, sra.time_window_hours as "time_window_hours!: u32",
|
||||
sra.analysis_parameters, sra.calculated_at as "calculated_at!"
|
||||
FROM simulated_route_analysis sra
|
||||
JOIN simulated_reward_epochs sre ON sra.simulated_epoch_id = sre.id
|
||||
WHERE sre.epoch_id = ? AND sra.calculation_method = ?
|
||||
"#,
|
||||
epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get node performance history across simulation epochs
|
||||
pub(crate) async fn get_simulated_node_performance_history(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT snp.id as "id!", snp.simulated_epoch_id as "simulated_epoch_id!",
|
||||
snp.node_id as "node_id!: NodeId", snp.node_type as "node_type!",
|
||||
snp.identity_key, snp.reliability_score as "reliability_score!",
|
||||
snp.positive_samples as "positive_samples!: u32", snp.negative_samples as "negative_samples!: u32",
|
||||
snp.work_factor, snp.calculation_method as "calculation_method!", snp.calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance snp
|
||||
JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id
|
||||
WHERE snp.node_id = ?
|
||||
ORDER BY sre.epoch_id DESC, snp.calculation_method
|
||||
"#,
|
||||
node_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ pub(crate) mod manager;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod runtime_migrations;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DbIdCache {
|
||||
pub mixnodes_v1: DashMap<NodeId, i64>,
|
||||
@@ -940,8 +943,9 @@ impl NymApiStorage {
|
||||
/// * `until`: timestamp specifying the purge cutoff.
|
||||
pub(crate) async fn purge_old_statuses(&self, until: i64) -> Result<(), NymApiStorageError> {
|
||||
self.manager.purge_old_mixnode_statuses(until).await?;
|
||||
self.manager.purge_old_gateway_statuses(until).await?;
|
||||
self.manager
|
||||
.purge_old_gateway_statuses(until)
|
||||
.purge_old_routes(until)
|
||||
.await
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
@@ -1045,5 +1049,23 @@ pub(crate) mod v3_migration {
|
||||
pub(crate) async fn make_node_id_not_null(&self) -> Result<(), NymApiStorageError> {
|
||||
Ok(self.manager.make_node_id_not_null().await?)
|
||||
}
|
||||
|
||||
/// Get mixnode identity key by node ID
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn get_mixnode_identity_key(
|
||||
&self,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Option<String>, NymApiStorageError> {
|
||||
Ok(self.manager.get_mixnode_identity_key(mix_id).await?)
|
||||
}
|
||||
|
||||
/// Get gateway identity key by node ID
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn get_gateway_identity_key(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Option<String>, NymApiStorageError> {
|
||||
Ok(self.manager.get_gateway_identity_key(node_id).await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,3 +146,82 @@ pub struct HistoricalUptime {
|
||||
pub date: Date,
|
||||
pub uptime: i64,
|
||||
}
|
||||
|
||||
// Simulated Rewarding System Models
|
||||
// These models support comparison between old (24h cache-based) and new (1h route-based) rewarding
|
||||
|
||||
/// Represents a simulated reward epoch run
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedRewardEpoch {
|
||||
pub id: i64,
|
||||
pub epoch_id: u32,
|
||||
pub calculation_method: String, // 'old', 'new', or 'comparison'
|
||||
pub start_timestamp: i64,
|
||||
pub end_timestamp: i64,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
/// Node performance calculated using different methodologies
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedNodePerformance {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String, // 'mixnode' or 'gateway'
|
||||
pub identity_key: Option<String>,
|
||||
pub reliability_score: f64, // 0.0 to 100.0
|
||||
pub positive_samples: u32,
|
||||
pub negative_samples: u32,
|
||||
pub work_factor: Option<f64>, // 0.0 to 1.0
|
||||
pub calculation_method: String, // 'old' or 'new'
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
/// Performance comparison data for analyzing methodology differences
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedPerformanceComparison {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String, // 'mixnode' or 'gateway'
|
||||
pub performance_score: f64, // 0.0 to 100.0
|
||||
pub work_factor: f64, // Work factor applied (e.g., 10.0 for active, 1.0 for standby)
|
||||
pub calculation_method: String, // 'old' or 'new'
|
||||
pub positive_samples: Option<i64>,
|
||||
pub negative_samples: Option<i64>,
|
||||
pub route_success_rate: Option<f64>, // 0.0 to 100.0, mainly for new method
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
/// Performance ranking data for nodes
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedPerformanceRanking {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub node_id: NodeId,
|
||||
pub calculation_method: String,
|
||||
pub performance_rank: i64,
|
||||
pub performance_percentile: f64,
|
||||
#[allow(dead_code)]
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
/// Route analysis metadata for simulation runs
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedRouteAnalysis {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub calculation_method: String, // 'old' or 'new'
|
||||
pub total_routes_analyzed: u32,
|
||||
pub successful_routes: u32,
|
||||
pub failed_routes: u32,
|
||||
pub average_route_reliability: Option<f64>, // 0.0 to 100.0
|
||||
pub time_window_hours: u32, // 1 for new method, 24 for old method
|
||||
pub analysis_parameters: Option<String>, // JSON with analysis config
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(test)]
|
||||
mod simulation_storage_tests {
|
||||
use super::super::models::{
|
||||
SimulatedNodePerformance, SimulatedPerformanceComparison, SimulatedRouteAnalysis,
|
||||
};
|
||||
use super::super::NymApiStorage;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
async fn create_test_storage() -> NymApiStorage {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let temp_path = temp_file.path().to_path_buf();
|
||||
|
||||
// Keep the temp file alive
|
||||
Box::leak(Box::new(temp_file));
|
||||
|
||||
NymApiStorage::init(temp_path).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulated_reward_epoch_crud() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Test create
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(
|
||||
100,
|
||||
"test",
|
||||
1234567890,
|
||||
1234571490,
|
||||
Some("Test description"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(epoch_id > 0);
|
||||
|
||||
// Test retrieve
|
||||
let retrieved = storage
|
||||
.manager
|
||||
.get_simulated_reward_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(retrieved.is_some());
|
||||
|
||||
let epoch = retrieved.unwrap();
|
||||
assert_eq!(epoch.epoch_id, 100);
|
||||
assert_eq!(epoch.calculation_method, "test");
|
||||
assert_eq!(epoch.start_timestamp, 1234567890);
|
||||
assert_eq!(epoch.end_timestamp, 1234571490);
|
||||
assert_eq!(epoch.description, Some("Test description".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulated_node_performance_crud() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create parent epoch first
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "test", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test insert node performance
|
||||
let performance = SimulatedNodePerformance {
|
||||
id: 0, // Will be set by database
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: Some("test_key".to_string()),
|
||||
reliability_score: 85.5,
|
||||
positive_samples: 100,
|
||||
negative_samples: 15,
|
||||
work_factor: Some(0.95),
|
||||
calculation_method: "new".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[performance])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test retrieve
|
||||
let retrieved = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_for_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(retrieved.len(), 1);
|
||||
let perf = &retrieved[0];
|
||||
assert_eq!(perf.node_id, 42);
|
||||
assert_eq!(perf.node_type, "mixnode");
|
||||
assert_eq!(perf.reliability_score, 85.5);
|
||||
assert_eq!(perf.calculation_method, "new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulated_performance_comparisons_crud() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create parent epoch first
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "test", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test insert performance comparison
|
||||
let comparison = SimulatedPerformanceComparison {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
performance_score: 85.0,
|
||||
work_factor: 10.0,
|
||||
calculation_method: "new".to_string(),
|
||||
positive_samples: Some(100),
|
||||
negative_samples: Some(15),
|
||||
route_success_rate: Some(85.0),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_performance_comparisons(&[comparison])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test retrieve
|
||||
let retrieved = storage
|
||||
.manager
|
||||
.get_simulated_performance_comparisons_for_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(retrieved.len(), 1);
|
||||
let comp = &retrieved[0];
|
||||
assert_eq!(comp.node_id, 42);
|
||||
assert_eq!(comp.performance_score, 85.0);
|
||||
assert_eq!(comp.work_factor, 10.0);
|
||||
assert_eq!(comp.calculation_method, "new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulated_route_analysis_crud() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create parent epoch first
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "test", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test insert route analysis
|
||||
let analysis = SimulatedRouteAnalysis {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
calculation_method: "new".to_string(),
|
||||
total_routes_analyzed: 1000,
|
||||
successful_routes: 950,
|
||||
failed_routes: 50,
|
||||
average_route_reliability: Some(95.0),
|
||||
time_window_hours: 1,
|
||||
analysis_parameters: Some("{\"threshold\": 0.8}".to_string()),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_route_analysis(&analysis)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test retrieve
|
||||
let retrieved = storage
|
||||
.manager
|
||||
.get_simulated_route_analysis_for_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(retrieved.len(), 1);
|
||||
let route = &retrieved[0];
|
||||
assert_eq!(route.calculation_method, "new");
|
||||
assert_eq!(route.total_routes_analyzed, 1000);
|
||||
assert_eq!(route.successful_routes, 950);
|
||||
assert_eq!(route.average_route_reliability, Some(95.0));
|
||||
assert_eq!(route.time_window_hours, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_foreign_key_constraints() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Try to insert node performance without valid epoch - should fail
|
||||
let performance = SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: 999999, // Non-existent epoch
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: None,
|
||||
reliability_score: 85.5,
|
||||
positive_samples: 100,
|
||||
negative_samples: 15,
|
||||
work_factor: Some(0.95),
|
||||
calculation_method: "new".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
let result = storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[performance])
|
||||
.await;
|
||||
assert!(result.is_err()); // Should fail due to foreign key constraint
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_performance_by_method_queries() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create epoch
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "comparison", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert performance data for both methods
|
||||
let old_performance = SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: Some("key42".to_string()),
|
||||
reliability_score: 80.0,
|
||||
positive_samples: 100,
|
||||
negative_samples: 20,
|
||||
work_factor: Some(0.9),
|
||||
calculation_method: "old".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
let new_performance = SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: Some("key42".to_string()),
|
||||
reliability_score: 90.0,
|
||||
positive_samples: 95,
|
||||
negative_samples: 5,
|
||||
work_factor: Some(0.95),
|
||||
calculation_method: "new".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
};
|
||||
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[old_performance])
|
||||
.await
|
||||
.unwrap();
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[new_performance])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test querying by method
|
||||
let old_results = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_by_method(100, "old")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_results = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_by_method(100, "new")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(old_results.len(), 1);
|
||||
assert_eq!(new_results.len(), 1);
|
||||
|
||||
assert_eq!(old_results[0].reliability_score, 80.0);
|
||||
assert_eq!(new_results[0].reliability_score, 90.0);
|
||||
|
||||
assert_eq!(old_results[0].calculation_method, "old");
|
||||
assert_eq!(new_results[0].calculation_method, "new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_node_performance_history() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create multiple epochs
|
||||
let epoch1_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "comparison", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let epoch2_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(101, "comparison", 1234571490, 1234575090, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert performance data for same node across epochs
|
||||
let performances = vec![
|
||||
SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch1_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: Some("key42".to_string()),
|
||||
reliability_score: 80.0,
|
||||
positive_samples: 100,
|
||||
negative_samples: 20,
|
||||
work_factor: Some(0.9),
|
||||
calculation_method: "old".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
},
|
||||
SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch2_id,
|
||||
node_id: 42,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: Some("key42".to_string()),
|
||||
reliability_score: 85.0,
|
||||
positive_samples: 105,
|
||||
negative_samples: 15,
|
||||
work_factor: Some(0.92),
|
||||
calculation_method: "old".to_string(),
|
||||
calculated_at: 1234571490,
|
||||
},
|
||||
];
|
||||
|
||||
for perf in performances {
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[perf])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test node history query
|
||||
let history = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_history(42)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(history.len(), 2);
|
||||
|
||||
// Should be ordered by epoch_id DESC
|
||||
assert_eq!(history[0].reliability_score, 85.0); // epoch 101
|
||||
assert_eq!(history[1].reliability_score, 80.0); // epoch 100
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_count_operations() {
|
||||
let storage = create_test_storage().await;
|
||||
|
||||
// Create epoch
|
||||
let epoch_id = storage
|
||||
.manager
|
||||
.create_simulated_reward_epoch(100, "test", 1234567890, 1234571490, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Initially should be 0
|
||||
let count = storage
|
||||
.manager
|
||||
.count_simulated_node_performance_for_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
|
||||
// Insert some performance data
|
||||
let performances = vec![
|
||||
SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 1,
|
||||
node_type: "mixnode".to_string(),
|
||||
identity_key: None,
|
||||
reliability_score: 80.0,
|
||||
positive_samples: 100,
|
||||
negative_samples: 20,
|
||||
work_factor: Some(0.9),
|
||||
calculation_method: "old".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
},
|
||||
SimulatedNodePerformance {
|
||||
id: 0,
|
||||
simulated_epoch_id: epoch_id,
|
||||
node_id: 2,
|
||||
node_type: "gateway".to_string(),
|
||||
identity_key: None,
|
||||
reliability_score: 90.0,
|
||||
positive_samples: 120,
|
||||
negative_samples: 10,
|
||||
work_factor: None,
|
||||
calculation_method: "new".to_string(),
|
||||
calculated_at: 1234567890,
|
||||
},
|
||||
];
|
||||
|
||||
for perf in performances {
|
||||
storage
|
||||
.manager
|
||||
.insert_simulated_node_performance(&[perf])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Should now be 2
|
||||
let count = storage
|
||||
.manager
|
||||
.count_simulated_node_performance_for_epoch(epoch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
}
|
||||
@@ -24,3 +24,6 @@ mod unstable_nym_nodes;
|
||||
|
||||
#[path = "public-api/unstable_status.rs"]
|
||||
mod unstable_status;
|
||||
|
||||
#[path = "public-api/simulation.rs"]
|
||||
mod simulation;
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
use crate::utils::{base_url, make_request, validate_json_response};
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_epochs_endpoint() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
println!("NYM_API not set, skipping simulation API tests");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let url = format!("{}/v1/simulation/epochs", base);
|
||||
let response = make_request(&url).await;
|
||||
|
||||
match response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
match json {
|
||||
Ok(data) => {
|
||||
// Verify response structure
|
||||
assert!(data.is_object());
|
||||
assert!(data.get("epochs").is_some());
|
||||
assert!(data.get("total_count").is_some());
|
||||
|
||||
let epochs = data.get("epochs").unwrap();
|
||||
assert!(epochs.is_array());
|
||||
|
||||
// If there are epochs, verify their structure
|
||||
if let Some(epoch_array) = epochs.as_array() {
|
||||
if !epoch_array.is_empty() {
|
||||
let first_epoch = &epoch_array[0];
|
||||
verify_epoch_summary_structure(first_epoch);
|
||||
}
|
||||
}
|
||||
|
||||
println!("✓ Simulation epochs endpoint returned valid response");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to parse JSON response: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// API might not be running in simulation mode, which is fine for tests
|
||||
println!("⚠ Simulation API not available (expected if not in simulation mode)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_epochs_with_pagination() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let url = format!("{}/v1/simulation/epochs?limit=5&offset=0", base);
|
||||
let response = make_request(&url).await;
|
||||
|
||||
match response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
match json {
|
||||
Ok(data) => {
|
||||
assert!(data.is_object());
|
||||
assert!(data.get("epochs").is_some());
|
||||
assert!(data.get("total_count").is_some());
|
||||
|
||||
let epochs = data.get("epochs").unwrap().as_array().unwrap();
|
||||
assert!(epochs.len() <= 5); // Should respect limit
|
||||
|
||||
println!("✓ Simulation epochs pagination works correctly");
|
||||
}
|
||||
Err(_) => {
|
||||
println!("⚠ Simulation API not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("⚠ Simulation API not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_epoch_details_structure() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// First, get a list of epochs to find a valid ID
|
||||
let epochs_url = format!("{}/v1/simulation/epochs?limit=1", base);
|
||||
let epochs_response = make_request(&epochs_url).await;
|
||||
|
||||
match epochs_response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
if let Ok(data) = json {
|
||||
let epochs = data.get("epochs").unwrap().as_array().unwrap();
|
||||
if !epochs.is_empty() {
|
||||
let first_epoch = &epochs[0];
|
||||
let epoch_id = first_epoch.get("id").unwrap().as_i64().unwrap();
|
||||
|
||||
// Now test the details endpoint
|
||||
let details_url = format!("{}/v1/simulation/epochs/{}", base, epoch_id);
|
||||
let details_response = make_request(&details_url).await;
|
||||
|
||||
match details_response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
match json {
|
||||
Ok(data) => {
|
||||
verify_epoch_details_structure(&data);
|
||||
println!("✓ Simulation epoch details endpoint returned valid structure");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to parse epoch details: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to fetch epoch details: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("⚠ Simulation API not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_comparison_endpoint() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// First, get a list of epochs to find a valid ID
|
||||
let epochs_url = format!("{}/v1/simulation/epochs?limit=1", base);
|
||||
let epochs_response = make_request(&epochs_url).await;
|
||||
|
||||
match epochs_response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
if let Ok(data) = json {
|
||||
let epochs = data.get("epochs").unwrap().as_array().unwrap();
|
||||
if !epochs.is_empty() {
|
||||
let first_epoch = &epochs[0];
|
||||
let epoch_id = first_epoch.get("id").unwrap().as_i64().unwrap();
|
||||
|
||||
// Test the comparison endpoint
|
||||
let comparison_url = format!("{}/v1/simulation/epochs/{}/comparison", base, epoch_id);
|
||||
let comparison_response = make_request(&comparison_url).await;
|
||||
|
||||
match comparison_response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
match json {
|
||||
Ok(data) => {
|
||||
verify_comparison_structure(&data);
|
||||
println!("✓ Simulation comparison endpoint returned valid structure");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to parse comparison data: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Failed to fetch comparison data: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("⚠ Simulation API not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_export_endpoints() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// First, get a list of epochs to find a valid ID
|
||||
let epochs_url = format!("{}/v1/simulation/epochs?limit=1", base);
|
||||
let epochs_response = make_request(&epochs_url).await;
|
||||
|
||||
match epochs_response {
|
||||
Ok(res) => {
|
||||
let json = validate_json_response(res).await;
|
||||
if let Ok(data) = json {
|
||||
let epochs = data.get("epochs").unwrap().as_array().unwrap();
|
||||
if !epochs.is_empty() {
|
||||
let first_epoch = &epochs[0];
|
||||
let epoch_id = first_epoch.get("id").unwrap().as_i64().unwrap();
|
||||
|
||||
// Test JSON export
|
||||
let json_export_url = format!("{}/v1/simulation/epochs/{}/export?format=json", base, epoch_id);
|
||||
let json_response = make_request(&json_export_url).await;
|
||||
|
||||
match json_response {
|
||||
Ok(res) => {
|
||||
assert!(res.status().is_success());
|
||||
let content_type = res.headers().get("content-type");
|
||||
if let Some(ct) = content_type {
|
||||
assert!(ct.to_str().unwrap().contains("application/json"));
|
||||
}
|
||||
println!("✓ JSON export endpoint works correctly");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ JSON export failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Test CSV export
|
||||
let csv_export_url = format!("{}/v1/simulation/epochs/{}/export?format=csv", base, epoch_id);
|
||||
let csv_response = make_request(&csv_export_url).await;
|
||||
|
||||
match csv_response {
|
||||
Ok(res) => {
|
||||
assert!(res.status().is_success());
|
||||
let content_type = res.headers().get("content-type");
|
||||
if let Some(ct) = content_type {
|
||||
assert!(ct.to_str().unwrap().contains("text/csv"));
|
||||
}
|
||||
println!("✓ CSV export endpoint works correctly");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ CSV export failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("⚠ Simulation API not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulation_error_handling() {
|
||||
let base = match base_url() {
|
||||
Ok(url) => url,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Test 404 for non-existent epoch
|
||||
let invalid_url = format!("{}/v1/simulation/epochs/999999", base);
|
||||
let response = make_request(&invalid_url).await;
|
||||
|
||||
match response {
|
||||
Ok(res) => {
|
||||
// Should get a successful response (empty or error structure)
|
||||
assert!(res.status().is_success() || res.status().is_client_error());
|
||||
println!("✓ Error handling works for invalid epoch IDs");
|
||||
}
|
||||
Err(_) => {
|
||||
// This is also acceptable if the endpoint returns an error
|
||||
println!("✓ Error handling works for invalid epoch IDs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to verify response structures
|
||||
|
||||
fn verify_epoch_summary_structure(epoch: &Value) {
|
||||
assert!(epoch.is_object());
|
||||
assert!(epoch.get("id").is_some());
|
||||
assert!(epoch.get("epoch_id").is_some());
|
||||
assert!(epoch.get("calculation_method").is_some());
|
||||
assert!(epoch.get("start_timestamp").is_some());
|
||||
assert!(epoch.get("end_timestamp").is_some());
|
||||
assert!(epoch.get("created_at").is_some());
|
||||
assert!(epoch.get("nodes_analyzed").is_some());
|
||||
assert!(epoch.get("available_methods").is_some());
|
||||
|
||||
// Verify types
|
||||
assert!(epoch.get("id").unwrap().is_i64());
|
||||
assert!(epoch.get("epoch_id").unwrap().is_u64());
|
||||
assert!(epoch.get("calculation_method").unwrap().is_string());
|
||||
assert!(epoch.get("nodes_analyzed").unwrap().is_u64());
|
||||
assert!(epoch.get("available_methods").unwrap().is_array());
|
||||
}
|
||||
|
||||
fn verify_epoch_details_structure(data: &Value) {
|
||||
assert!(data.is_object());
|
||||
assert!(data.get("epoch").is_some());
|
||||
assert!(data.get("node_performance").is_some());
|
||||
assert!(data.get("rewards").is_some());
|
||||
assert!(data.get("route_analysis").is_some());
|
||||
|
||||
// Verify epoch summary structure
|
||||
verify_epoch_summary_structure(data.get("epoch").unwrap());
|
||||
|
||||
// Verify arrays
|
||||
assert!(data.get("node_performance").unwrap().is_array());
|
||||
assert!(data.get("rewards").unwrap().is_array());
|
||||
assert!(data.get("route_analysis").unwrap().is_array());
|
||||
|
||||
// If there's performance data, verify its structure
|
||||
let performance_array = data.get("node_performance").unwrap().as_array().unwrap();
|
||||
if !performance_array.is_empty() {
|
||||
let first_performance = &performance_array[0];
|
||||
verify_performance_data_structure(first_performance);
|
||||
}
|
||||
|
||||
// If there's reward data, verify its structure
|
||||
let rewards_array = data.get("rewards").unwrap().as_array().unwrap();
|
||||
if !rewards_array.is_empty() {
|
||||
let first_reward = &rewards_array[0];
|
||||
verify_reward_data_structure(first_reward);
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_performance_data_structure(performance: &Value) {
|
||||
assert!(performance.is_object());
|
||||
assert!(performance.get("node_id").is_some());
|
||||
assert!(performance.get("node_type").is_some());
|
||||
assert!(performance.get("reliability_score").is_some());
|
||||
assert!(performance.get("positive_samples").is_some());
|
||||
assert!(performance.get("negative_samples").is_some());
|
||||
assert!(performance.get("calculation_method").is_some());
|
||||
assert!(performance.get("calculated_at").is_some());
|
||||
|
||||
// Verify types
|
||||
assert!(performance.get("node_id").unwrap().is_u64());
|
||||
assert!(performance.get("node_type").unwrap().is_string());
|
||||
assert!(performance.get("reliability_score").unwrap().is_f64());
|
||||
assert!(performance.get("positive_samples").unwrap().is_u64());
|
||||
assert!(performance.get("negative_samples").unwrap().is_u64());
|
||||
assert!(performance.get("calculation_method").unwrap().is_string());
|
||||
assert!(performance.get("calculated_at").unwrap().is_i64());
|
||||
}
|
||||
|
||||
fn verify_reward_data_structure(reward: &Value) {
|
||||
assert!(reward.is_object());
|
||||
assert!(reward.get("node_id").is_some());
|
||||
assert!(reward.get("node_type").is_some());
|
||||
assert!(reward.get("calculated_reward_amount").is_some());
|
||||
assert!(reward.get("reward_currency").is_some());
|
||||
assert!(reward.get("calculation_method").is_some());
|
||||
|
||||
// Verify types
|
||||
assert!(reward.get("node_id").unwrap().is_u64());
|
||||
assert!(reward.get("node_type").unwrap().is_string());
|
||||
assert!(reward.get("calculated_reward_amount").unwrap().is_f64());
|
||||
assert!(reward.get("reward_currency").unwrap().is_string());
|
||||
assert!(reward.get("calculation_method").unwrap().is_string());
|
||||
}
|
||||
|
||||
fn verify_comparison_structure(data: &Value) {
|
||||
assert!(data.is_object());
|
||||
assert!(data.get("epoch_id").is_some());
|
||||
assert!(data.get("simulation_epoch_id").is_some());
|
||||
assert!(data.get("node_comparisons").is_some());
|
||||
assert!(data.get("summary_statistics").is_some());
|
||||
assert!(data.get("route_analysis_comparison").is_some());
|
||||
|
||||
// Verify types
|
||||
assert!(data.get("epoch_id").unwrap().is_u64());
|
||||
assert!(data.get("simulation_epoch_id").unwrap().is_i64());
|
||||
assert!(data.get("node_comparisons").unwrap().is_array());
|
||||
assert!(data.get("summary_statistics").unwrap().is_object());
|
||||
assert!(data.get("route_analysis_comparison").unwrap().is_object());
|
||||
|
||||
// Verify summary statistics structure
|
||||
let stats = data.get("summary_statistics").unwrap();
|
||||
assert!(stats.get("total_nodes_compared").is_some());
|
||||
assert!(stats.get("nodes_improved").is_some());
|
||||
assert!(stats.get("nodes_degraded").is_some());
|
||||
assert!(stats.get("average_reliability_old").is_some());
|
||||
assert!(stats.get("average_reliability_new").is_some());
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-network-monitor"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import time
|
||||
from locust import HttpUser, task
|
||||
|
||||
|
||||
class SendMsg(HttpUser):
|
||||
@task
|
||||
def hello_world(self):
|
||||
self.client.post("/v1/send")
|
||||
try:
|
||||
response = self.client.post("/v1/send")
|
||||
if response.status_code == 503:
|
||||
time.sleep(1)
|
||||
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
||||
except Exception: # Catch other exceptions, including those raised by raise_for_status()
|
||||
# You might want to log this error or handle it differently
|
||||
pass
|
||||
|
||||
@@ -15,13 +15,13 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use tokio::{sync::RwLock, time::timeout};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
accounting::{all_node_stats, NetworkAccount, NetworkAccountStats, NodeStats},
|
||||
http::AppState,
|
||||
MIXNET_TIMEOUT,
|
||||
make_client, MIXNET_TIMEOUT, TOPOLOGY,
|
||||
};
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -183,7 +183,13 @@ pub async fn mermaid_handler() -> Result<String, StatusCode> {
|
||||
Ok(mermaid)
|
||||
}
|
||||
|
||||
async fn send_receive_mixnet(state: AppState) -> Result<String, StatusCode> {
|
||||
async fn send_receive_mixnet(_state: AppState) -> Result<String, StatusCode> {
|
||||
let client = Arc::new(RwLock::new(
|
||||
make_client(TOPOLOGY.get().expect("Set at the begining").clone())
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||
));
|
||||
|
||||
let msg: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
@@ -191,15 +197,15 @@ async fn send_receive_mixnet(state: AppState) -> Result<String, StatusCode> {
|
||||
.collect();
|
||||
let sent_msg = msg.clone();
|
||||
|
||||
let client = {
|
||||
let mut clients = state.clients().write().await;
|
||||
if let Some(client) = clients.make_contiguous().choose(&mut rand::thread_rng()) {
|
||||
Arc::clone(client)
|
||||
} else {
|
||||
error!("No clients currently available");
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
};
|
||||
// let client = {
|
||||
// let mut clients = state.clients().write().await;
|
||||
// if let Some(client) = clients.make_contiguous().choose(&mut rand::thread_rng()) {
|
||||
// Arc::clone(client)
|
||||
// } else {
|
||||
// error!("No clients currently available");
|
||||
// return Err(StatusCode::SERVICE_UNAVAILABLE);
|
||||
// }
|
||||
// };
|
||||
|
||||
let recv = Arc::clone(&client);
|
||||
let sender = Arc::clone(&client);
|
||||
|
||||
@@ -175,7 +175,11 @@ async fn nym_topology_forced_all_from_env() -> anyhow::Result<NymTopology> {
|
||||
let node_ids = topology
|
||||
.node_details()
|
||||
.iter()
|
||||
.filter(|(_node_id, node)| node.supported_roles.mixnode)
|
||||
.filter(|(_node_id, node)| {
|
||||
node.supported_roles.mixnode
|
||||
&& !node.supported_roles.mixnet_entry
|
||||
&& !node.supported_roles.mixnet_exit
|
||||
})
|
||||
.map(|(node_id, _)| *node_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -232,13 +236,13 @@ async fn main() -> Result<()> {
|
||||
|
||||
MIXNET_TIMEOUT.set(args.mixnet_timeout).ok();
|
||||
|
||||
let spawn_clients = Arc::clone(&clients);
|
||||
tokio::spawn(make_clients(
|
||||
spawn_clients,
|
||||
args.n_clients,
|
||||
args.client_lifetime,
|
||||
TOPOLOGY.get().expect("Topology not set yet!").clone(),
|
||||
));
|
||||
// let spawn_clients = Arc::clone(&clients);
|
||||
// tokio::spawn(make_clients(
|
||||
// spawn_clients,
|
||||
// args.n_clients,
|
||||
// args.client_lifetime,
|
||||
// TOPOLOGY.get().expect("Topology not set yet!").clone(),
|
||||
// ));
|
||||
|
||||
let clients_server = clients.clone();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user