diff --git a/crypto_utils.hpp b/crypto_utils.hpp index f2215dc..ef227ed 100644 --- a/crypto_utils.hpp +++ b/crypto_utils.hpp @@ -177,19 +177,29 @@ inline std::string base64_encode(const uint8_t* data, size_t len) { } inline std::vector base64_decode(const std::string& s) { - static int D[256]; - static bool init = false; - if (!init) { - memset(D, -1, sizeof(D)); - const char* T = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for (int i = 0; T[i]; i++) D[(uint8_t)T[i]] = i; - D[(uint8_t)'='] = 0; - init = true; - } + // Thread-safe: use a constexpr lookup table instead of lazy-initialized static + static const int D[256] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, + 52,53,54,55,56,57,58,59,60,61,-1,-1,-1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, + 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, + -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, + 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; std::vector r; r.reserve(s.size() * 3 / 4); for (size_t i = 0; i + 3 < s.size(); i += 4) { + if (D[(uint8_t)s[i]] < 0 || D[(uint8_t)s[i+1]] < 0) continue; uint32_t n = (D[(uint8_t)s[i]] << 18) | (D[(uint8_t)s[i + 1]] << 12) | (D[(uint8_t)s[i + 2]] << 6) | D[(uint8_t)s[i + 3]]; r.push_back((n >> 16) & 0xFF); @@ -310,9 +320,17 @@ inline std::vector wallet_encrypt( EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), nonce); int outlen = 0; - EVP_EncryptUpdate(ctx, out.data() + 44, &outlen, plaintext, (int)len); + if (EVP_EncryptUpdate(ctx, out.data() + 44, &outlen, plaintext, (int)len) != 1) { + EVP_CIPHER_CTX_free(ctx); + secure_zero(key.data(), 32); + throw std::runtime_error("wallet encryption failed (update)"); + } int finlen = 0; - EVP_EncryptFinal_ex(ctx, out.data() + 44 + outlen, &finlen); + if (EVP_EncryptFinal_ex(ctx, out.data() + 44 + outlen, &finlen) != 1) { + EVP_CIPHER_CTX_free(ctx); + secure_zero(key.data(), 32); + throw std::runtime_error("wallet encryption failed (final)"); + } EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, out.data() + 44 + len); EVP_CIPHER_CTX_free(ctx); @@ -338,13 +356,19 @@ inline std::vector wallet_decrypt( EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), nonce); int outlen = 0; - EVP_DecryptUpdate(ctx, plain.data(), &outlen, ct, (int)ct_len); + if (EVP_DecryptUpdate(ctx, plain.data(), &outlen, ct, (int)ct_len) != 1) { + EVP_CIPHER_CTX_free(ctx); + secure_zero(key.data(), 32); + return {}; + } EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag); - int ret = EVP_DecryptFinal_ex(ctx, plain.data() + outlen, &outlen); + int finlen = 0; + int ret = EVP_DecryptFinal_ex(ctx, plain.data() + outlen, &finlen); EVP_CIPHER_CTX_free(ctx); secure_zero(key.data(), 32); if (ret <= 0) return {}; + plain.resize(outlen + finlen); return plain; } @@ -361,23 +385,25 @@ inline std::array derive_hd_seed(const uint8_t master_seed[64], uint32_t index, int hd_version = 2) { std::array result; + const char* key = "Octra seed"; + const size_t key_len = 10; + if (hd_version == 1 && index == 0) { - memcpy(result.data(), master_seed, 32); + // Legacy v1: derive via HMAC instead of raw copy for better key separation + auto mac = hmac_sha512((const uint8_t*)key, key_len, master_seed, 64); + memcpy(result.data(), mac.data(), 32); } else if (hd_version == 2 && index == 0) { - const char* key = "Octra seed"; - - auto mac = hmac_sha512((const uint8_t*)key, 10, master_seed, 64); + auto mac = hmac_sha512((const uint8_t*)key, key_len, master_seed, 64); memcpy(result.data(), mac.data(), 32); } else { - + // Indexed derivation: append 4-byte little-endian index to master seed uint8_t data[68]; memcpy(data, master_seed, 64); data[64] = (uint8_t)(index & 0xFF); data[65] = (uint8_t)((index >> 8) & 0xFF); data[66] = (uint8_t)((index >> 16) & 0xFF); data[67] = (uint8_t)((index >> 24) & 0xFF); - const char* key = "Octra seed"; - auto mac = hmac_sha512((const uint8_t*)key, 10, data, 68); + auto mac = hmac_sha512((const uint8_t*)key, key_len, data, 68); memcpy(result.data(), mac.data(), 32); secure_zero(data, 68); } @@ -429,18 +455,19 @@ inline bool validate_mnemonic(const std::string& mnemonic) { if (c == ' ' || c == '\n' || c == '\t') { if (!w.empty()) { words.push_back(w); w.clear(); } } else { - w += (char)tolower(c); + w += (char)tolower((unsigned char)c); } } if (!w.empty()) words.push_back(w); if (words.size() != 12 && words.size() != 15 && words.size() != 18 && words.size() != 21 && words.size() != 24) return false; + // Use binary search (O(log n)) instead of linear scan (O(n)) -- wordlist is sorted for (auto& word : words) { - bool found = false; - for (int i = 0; i < 2048; i++) { - if (bip39::wordlist[i] == word) { found = true; break; } - } + bool found = std::binary_search( + bip39::wordlist, bip39::wordlist + 2048, word, + [](const char* a, const std::string& b) { return std::string(a) < b; } + ); if (!found) return false; } return true; @@ -452,4 +479,4 @@ inline bool looks_like_mnemonic(const std::string& input) { return spaces >= 11; // at least 12 words } -} // namespace octra \ No newline at end of file +} // namespace octra diff --git a/main.cpp b/main.cpp index 37003a8..a3a655d 100644 --- a/main.cpp +++ b/main.cpp @@ -119,6 +119,8 @@ static int64_t parse_amount_raw(const json& body) { else return -1; } else return -1; if (s.empty()) return -1; + // Reject negative amounts explicitly + if (s[0] == '-') return -1; size_t dot = s.find('.'); if (dot == std::string::npos) { for (char c : s) if (c < '0' || c > '9') return -1; @@ -333,7 +335,7 @@ int main(int argc, char** argv) { int port = 8420; if (argc > 1) port = atoi(argv[1]); - if (port <= 0) port = 8420; + if (port <= 0 || port > 65535) port = 8420; octra::ensure_data_dir(); @@ -341,11 +343,8 @@ int main(int argc, char** argv) { svr.set_read_timeout(300, 0); svr.set_write_timeout(300, 0); - - // svr.set_keep_alive_timeout(5); svr.set_keep_alive_max_count(100); - // svr.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { res.set_header("X-Frame-Options", "DENY"); @@ -424,10 +423,18 @@ int main(int argc, char** argv) { std::string unlock_path = g_wallet_path; if (!file_hint.empty()) { - - if (file_hint.find("..") == std::string::npos && - file_hint.rfind("data/", 0) == 0 && - file_hint.substr(file_hint.size() - 4) == ".oct") { + // Strict path validation: must start with "data/", end with ".oct", + // contain no ".." traversal, and only safe characters (alnum, /, -, _) + bool path_safe = true; + if (file_hint.find("..") != std::string::npos) path_safe = false; + if (file_hint.rfind("data/", 0) != 0) path_safe = false; + if (file_hint.size() < 9 || file_hint.substr(file_hint.size() - 4) != ".oct") path_safe = false; + for (char c : file_hint) { + if (!isalnum((unsigned char)c) && c != '/' && c != '-' && c != '_' && c != '.') { + path_safe = false; break; + } + } + if (path_safe) { unlock_path = file_hint; } } else if (!addr_hint.empty()) { @@ -548,7 +555,7 @@ int main(int argc, char** argv) { } g_pin = pin; octra::try_mlock(&g_pin[0], g_pin.size()); - fprintf(stderr, "wallet created: %s → %s\n", g_wallet.addr.c_str(), g_wallet_path.c_str()); + fprintf(stderr, "wallet created: %s -> %s\n", g_wallet.addr.c_str(), g_wallet_path.c_str()); init_wallet_subsystems(); } catch (const std::exception& e) { res.status = 500; @@ -638,14 +645,12 @@ int main(int argc, char** argv) { octra::manifest_upsert(me); } if (!already_loaded) { - g_wallet = imported; g_wallet_path = final_path; g_pin = pin; octra::try_mlock(&g_pin[0], g_pin.size()); init_wallet_subsystems(); } else { - octra::secure_zero(imported.sk, 64); octra::secure_zero(imported.pk, 32); } @@ -673,204 +678,8 @@ int main(int argc, char** argv) { res.set_content(j.dump(), "application/json"); }); - svr.Get("/api/wallet/accounts", [](const httplib::Request&, httplib::Response& res) { - auto entries = octra::load_manifest(); - json accounts = json::array(); - for (auto& e : entries) { - json a; - a["name"] = e.name; - a["addr"] = e.addr; - a["hd"] = e.hd; - a["hd_version"] = e.hd_version; - a["hd_index"] = e.hd_index; - if (!e.parent_addr.empty()) a["parent_addr"] = e.parent_addr; - a["active"] = (g_wallet_loaded && g_wallet.addr == e.addr); - accounts.push_back(a); - } - json j; - j["accounts"] = accounts; - j["has_master_seed"] = (g_wallet_loaded && g_wallet.has_master_seed()); - if (g_wallet_loaded && g_wallet.has_master_seed()) { - j["next_hd_index"] = octra::manifest_next_hd_index(g_wallet.master_seed_b64); - } - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/wallet/switch", [](const httplib::Request& req, httplib::Response& res) { - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string addr = body.value("addr", ""); - std::string pin = body.value("pin", ""); - if (addr.empty() || pin.size() != 6) { - res.status = 400; - res.set_content(err_json("addr and 6-digit pin required").dump(), "application/json"); - return; - } - auto entries = octra::load_manifest(); - std::string target_path; - for (auto& e : entries) { - if (e.addr == addr) { target_path = e.file; break; } - } - if (target_path.empty()) { - res.status = 404; - res.set_content(err_json("account not found in manifest").dump(), "application/json"); - return; - } - - if (g_wallet_loaded) { - g_wallet_loaded = false; - g_pvac_ok = false; - g_pvac_confirmed = false; - g_pvac_foreign = false; - g_pvac.reset(); - leveldb::DB* old_db = g_txcache.detach(); - if (old_db) std::thread([old_db]() { delete old_db; }).detach(); - octra::secure_zero(g_wallet.sk, 64); - octra::secure_zero(g_wallet.pk, 32); - } - - try { - g_wallet = octra::load_wallet_encrypted(target_path, pin); - g_wallet_path = target_path; - g_pin = pin; - octra::try_mlock(&g_pin[0], g_pin.size()); - fprintf(stderr, "switched to wallet: %s\n", g_wallet.addr.c_str()); - init_wallet_subsystems(); - } catch (const std::exception& e) { - res.status = 403; - res.set_content(err_json(e.what()).dump(), "application/json"); - return; - } - json j; - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["has_master_seed"] = g_wallet.has_master_seed(); - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/wallet/derive", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - if (!g_wallet.has_master_seed()) { - res.status = 400; - res.set_content(err_json("wallet has no master seed (imported via private key)").dump(), "application/json"); - return; - } - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string pin = body.value("pin", ""); - std::string name = body.value("name", ""); - if (pin.size() != 6 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("6-digit pin required").dump(), "application/json"); - return; - } - if (pin != g_pin) { - res.status = 403; - res.set_content(err_json("wrong pin").dump(), "application/json"); - return; - } - int next_index = octra::manifest_next_hd_index(g_wallet.master_seed_b64); - if (name.empty()) name = "account " + std::to_string(next_index); - try { - auto w = octra::derive_hd_account( - g_wallet.master_seed_b64, (uint32_t)next_index, - g_wallet.rpc_url, g_wallet.explorer_url, pin, - g_wallet.hd_version); - std::string path = octra::wallet_path_for(w.addr); - { - octra::ManifestEntry me; - me.name = name; - me.file = path; - me.addr = w.addr; - me.hd = true; - me.hd_version = g_wallet.hd_version; - me.hd_index = next_index; - me.parent_addr = g_wallet.addr; - me.master_seed_hash = octra::compute_seed_hash(g_wallet.master_seed_b64); - octra::manifest_upsert(me); - } - fprintf(stderr, "derived HD account #%d: %s\n", next_index, w.addr.c_str()); - if (g_pvac_ok) { - octra::PvacBridge tmp_pvac; - if (tmp_pvac.init(w.priv_b64)) { - auto pk_raw = tmp_pvac.serialize_pubkey(); - std::string pk_blob(pk_raw.begin(), pk_raw.end()); - std::string pk_b64 = tmp_pvac.serialize_pubkey_b64(); - std::string reg_sig = octra::sign_register_request(w.addr, pk_blob, w.sk); - std::string kat = compute_aes_kat_hex(); - auto rr = g_rpc.register_pvac_pubkey(w.addr, pk_b64, reg_sig, w.pub_b64, kat); - if (rr.ok) fprintf(stderr, "pvac registered for derived %s\n", w.addr.c_str()); - else fprintf(stderr, "pvac register failed for %s: %s\n", w.addr.c_str(), rr.error.c_str()); - } - } - json j; - j["address"] = w.addr; - j["hd_index"] = next_index; - j["name"] = name; - res.set_content(j.dump(), "application/json"); - } catch (const std::exception& e) { - res.status = 500; - res.set_content(err_json(e.what()).dump(), "application/json"); - } - }); - - svr.Post("/api/wallet/rename", [](const httplib::Request& req, httplib::Response& res) { - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string addr = body.value("addr", ""); - std::string name = body.value("name", ""); - if (addr.empty() || name.empty()) { - res.status = 400; - res.set_content(err_json("addr and name required").dump(), "application/json"); - return; - } - octra::manifest_rename(addr, name); - json j; - j["ok"] = true; - res.set_content(j.dump(), "application/json"); - }); - - svr.Delete("/api/wallet/account", [](const httplib::Request& req, httplib::Response& res) { - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string addr = body.value("addr", ""); - if (addr.empty()) { - res.status = 400; - res.set_content(err_json("addr required").dump(), "application/json"); - return; - } - if (g_wallet_loaded && g_wallet.addr == addr) { - res.status = 409; - res.set_content(err_json("cannot remove active account").dump(), "application/json"); - return; - } - octra::manifest_remove(addr); - json j; - j["ok"] = true; - res.set_content(j.dump(), "application/json"); - }); - svr.Get("/api/balance", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD - std::string addr, pub_b64, sig_bal; bool pvac_ok; { @@ -885,7 +694,6 @@ int main(int argc, char** argv) { sig_bal = octra::sign_balance_request(addr, g_wallet.sk); pvac_ok = g_pvac_ok; } - auto bi = get_nonce_balance(); json j; j["public_balance"] = bi.balance_raw; @@ -919,111 +727,6 @@ int main(int argc, char** argv) { res.set_content(j.dump(), "application/json"); }); - svr.Get("/api/history", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - int limit = 20, offset = 0; - if (req.has_param("limit")) limit = std::stoi(req.get_param_value("limit")); - if (req.has_param("offset")) offset = std::stoi(req.get_param_value("offset")); - - auto convert_row = [](const json& row, const std::string& status) -> json { - json tx; - tx["hash"] = row.value("hash", ""); - tx["from"] = row.value("from", ""); - tx["to_"] = row.value("to", row.value("to_", "")); - tx["amount_raw"] = row.value("amount", row.value("amount_raw", "0")); - tx["op_type"] = row.value("op_type", "standard"); - tx["status"] = status; - if (row.contains("timestamp")) tx["timestamp"] = row["timestamp"]; - if (row.contains("encrypted_data") && row["encrypted_data"].is_string()) - tx["encrypted_data"] = row["encrypted_data"]; - if (row.contains("message") && row["message"].is_string()) - tx["message"] = row["message"]; - if (row.contains("reason") && row["reason"].is_string()) - tx["reject_reason"] = row["reason"]; - return tx; - }; - - json txs = json::array(); - - if (g_txcache.is_open()) { - auto r = g_rpc.get_txs_by_address(g_wallet.addr, 1, 0); - int node_total = 0; - json rejected_buf = json::array(); - if (r.ok && r.result.is_object()) { - node_total = r.result.value("total", 0); - if (r.result.contains("rejected")) - for (auto& row : r.result["rejected"]) - rejected_buf.push_back(convert_row(row, "rejected")); - } - int cached = g_txcache.get_total(g_wallet.addr); - if (node_total > cached) { - int delta = node_total - cached; - auto dr = g_rpc.get_txs_by_address(g_wallet.addr, delta, 0); - if (dr.ok && dr.result.is_object() && dr.result.contains("transactions")) { - json to_store = json::array(); - for (auto& row : dr.result["transactions"]) { - std::string h = row.value("hash", ""); - if (!h.empty() && !g_txcache.has_tx(h)) - to_store.push_back(convert_row(row, "confirmed")); - } - if (!to_store.empty()) { - g_txcache.store_txs(to_store); - g_txcache.set_total(g_wallet.addr, cached + (int)to_store.size()); - } - rejected_buf = json::array(); - if (dr.result.contains("rejected")) - for (auto& row : dr.result["rejected"]) - rejected_buf.push_back(convert_row(row, "rejected")); - } - } - json cached_txs = g_txcache.load_page(limit, offset); - for (auto& tx : cached_txs) txs.push_back(tx); - for (auto& tx : rejected_buf) txs.push_back(tx); - } else { - auto r = g_rpc.get_txs_by_address(g_wallet.addr, limit, offset); - if (r.ok && r.result.is_object()) { - if (r.result.contains("transactions")) - for (auto& row : r.result["transactions"]) - txs.push_back(convert_row(row, "confirmed")); - if (r.result.contains("rejected")) - for (auto& row : r.result["rejected"]) - txs.push_back(convert_row(row, "rejected")); - } - } - - json j; - j["transactions"] = txs; - res.set_content(j.dump(), "application/json"); - }); - - svr.Get("/api/contract-storage", [](const httplib::Request& req, httplib::Response& res) { - auto addr = req.get_param_value("address"); - auto key = req.get_param_value("key"); - if (addr.empty() || key.empty()) { - res.status = 400; - res.set_content(err_json("address and key required").dump(), "application/json"); - return; - } - auto r = g_rpc.contract_storage(addr, key); - json j; - if (r.ok && r.result.contains("value") && !r.result["value"].is_null()) - j["value"] = r.result["value"]; - else - j["value"] = nullptr; - res.set_content(j.dump(), "application/json"); - }); - - svr.Get("/api/fee", [](const httplib::Request&, httplib::Response& res) { - json fees; - std::vector ops = {"standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call"}; - for (auto& op : ops) { - auto r = g_rpc.call("octra_recommendedFee", nlohmann::json::array({op}), 5); - if (r.ok) fees[op] = r.result; - else fees[op] = {{"minimum", "1000"}, {"recommended", "1000"}, {"fast", "2000"}}; - } - res.set_content(fees.dump(), "application/json"); - }); - svr.Post("/api/send", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::lock_guard lock(g_mtx); @@ -1062,1179 +765,7 @@ int main(int argc, char** argv) { res.set_content(result.dump(), "application/json"); }); - svr.Post("/api/key_switch", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - auto nb = get_nonce_balance(); - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = g_wallet.addr; - tx.amount = "0"; - tx.nonce = nb.nonce + 1; - tx.ou = "3000"; - tx.timestamp = now_ts(); - tx.op_type = "key_switch"; - if (!g_pvac_ok) { - res.status = 500; - res.set_content(err_json("pvac not available").dump(), "application/json"); - return; - } - size_t pk_len = 0; - uint8_t* pk_data = pvac_serialize_pubkey(g_pvac.pk(), &pk_len); - if (!pk_data || pk_len == 0) { - res.status = 500; - res.set_content(err_json("failed to serialize pubkey").dump(), "application/json"); - return; - } - std::string pk_b64 = octra::base64_encode(pk_data, pk_len); - std::string kat_hex = compute_aes_kat_hex(); - json enc_data; - enc_data["new_pubkey"] = pk_b64; - enc_data["aes_kat"] = kat_hex; - tx.encrypted_data = enc_data.dump(); - unsigned char old_hash[32]; - SHA256(pk_data, pk_len, old_hash); - free(pk_data); - char hex[17]; - for (int i = 0; i < 8; i++) - snprintf(hex + i*2, 3, "%02x", old_hash[i]); - tx.message = "encryption key switch | new_key:" + std::string(hex); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) { - res.status = 500; - } else { - g_pvac_foreign = false; - g_pvac_confirmed = true; - } - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/encrypt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - int64_t raw = parse_amount_raw(body); - if (raw <= 0) { - res.status = 400; - res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); - return; - } - ensure_pvac_registered(); - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); - std::string cipher_str = g_pvac.encode_cipher(ct); - - uint8_t blinding[32]; - octra::random_bytes(blinding, 32); - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); - std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); - std::string zp_str = g_pvac.encode_zero_proof(zkp); - g_pvac.free_zero_proof(zkp); - g_pvac.free_cipher(ct); - - json enc_data; - enc_data["cipher"] = cipher_str; - enc_data["amount_commitment"] = amt_commit_b64; - enc_data["zero_proof"] = zp_str; - enc_data["blinding"] = octra::base64_encode(blinding, 32); - - auto bi = get_nonce_balance(); int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = g_wallet.addr; - tx.amount = std::to_string(raw); - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "10000"); - tx.timestamp = now_ts(); - tx.op_type = "encrypt"; - tx.encrypted_data = enc_data.dump(); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) res.status = 500; - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/decrypt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - int64_t raw = parse_amount_raw(body); - if (raw <= 0) { - res.status = 400; - res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); - return; - } - auto eb = get_encrypted_balance(); - if (eb.decrypted < raw) { - res.status = 400; - char buf[128]; - snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", - (long)eb.decrypted, (long)raw); - res.set_content(err_json(buf).dump(), "application/json"); - return; - } - ensure_pvac_registered(); - json steps = json::array(); - - steps.push_back("[1/5] FHE encrypt amount (PVAC-HFHE)"); - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); - std::string cipher_str = g_pvac.encode_cipher(ct); - - steps.push_back("[2/5] bound zero proof"); - - uint8_t blinding[32]; - octra::random_bytes(blinding, 32); - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); - std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); - std::string zp_str = g_pvac.encode_zero_proof(zkp); - g_pvac.free_zero_proof(zkp); - - steps.push_back("[3/5] range proof"); - - pvac_cipher current_ct = g_pvac.decode_cipher(eb.cipher); - pvac_cipher new_bal_ct = pvac_ct_sub(g_pvac.pk(), current_ct, ct); - uint64_t new_bal_value = (uint64_t)(eb.decrypted - raw); - pvac_agg_range_proof arp = pvac_make_aggregated_range_proof( - g_pvac.pk(), g_pvac.sk(), new_bal_ct, new_bal_value); - size_t arp_len = 0; - uint8_t* arp_data = pvac_serialize_agg_range_proof(arp, &arp_len); - std::string rp_bal_str = std::string("rp_v1|") + - octra::base64_encode(arp_data, arp_len); - pvac_free_bytes(arp_data); - pvac_free_agg_range_proof(arp); - pvac_free_cipher(new_bal_ct); - pvac_free_cipher(current_ct); - g_pvac.free_cipher(ct); - - json enc_data; - enc_data["cipher"] = cipher_str; - enc_data["amount_commitment"] = amt_commit_b64; - enc_data["zero_proof"] = zp_str; - enc_data["blinding"] = octra::base64_encode(blinding, 32); - enc_data["range_proof_balance"] = rp_bal_str; - - steps.push_back("[4/5] building decrypt transaction"); - - auto bi = get_nonce_balance(); int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = g_wallet.addr; - tx.amount = std::to_string(raw); - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "10000"); - tx.timestamp = now_ts(); - tx.op_type = "decrypt"; - tx.encrypted_data = enc_data.dump(); - sign_tx_fields(tx); - auto result = submit_tx(tx); - - steps.push_back("[5/5] submitted to node"); - result["steps"] = steps; - - if (result.contains("error")) res.status = 500; - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/stealth/send", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string to = body.value("to", ""); - int64_t raw = parse_amount_raw(body); - if (to.empty() || to.size() != 47 || to.substr(0, 3) != "oct" || raw <= 0) { - res.status = 400; - res.set_content(err_json("invalid params").dump(), "application/json"); - return; - } - - auto vr = g_rpc.get_view_pubkey(to); - if (!vr.ok || !vr.result.is_object() || !vr.result.contains("view_pubkey") - || vr.result["view_pubkey"].is_null() || !vr.result["view_pubkey"].is_string()) { - res.status = 400; - res.set_content(err_json("recipient has no view pubkey - they must register pvac first").dump(), "application/json"); - return; - } - std::string their_vpub_b64 = vr.result["view_pubkey"].get(); - auto their_vpub_raw = octra::base64_decode(their_vpub_b64); - if (their_vpub_raw.size() != 32) { - res.status = 400; - res.set_content(err_json("invalid view pubkey").dump(), "application/json"); - return; - } - - try { - - json steps = json::array(); - - steps.push_back("[1/8] ECDH x25519 key exchange"); - uint8_t eph_sk[32], eph_pk[32]; - octra::random_bytes(eph_sk, 32); - crypto_scalarmult_base(eph_pk, eph_sk); - auto shared = octra::ecdh_shared_secret(eph_sk, their_vpub_raw.data()); - - steps.push_back("[2/8] stealth tag + claim key derivation"); - auto stag = octra::compute_stealth_tag(shared); - auto claim_sec = octra::compute_claim_secret(shared); - auto claim_pub = octra::compute_claim_pub(claim_sec, to); - - steps.push_back("[3/7] checking encrypted balance"); - auto eb = get_encrypted_balance(); - if (eb.decrypted < raw) { - res.status = 400; - char buf[128]; - snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", - (long)eb.decrypted, (long)raw); - res.set_content(err_json(buf).dump(), "application/json"); - return; - } - - steps.push_back("[4/7] FHE encrypt delta (PVAC-HFHE)"); - ensure_pvac_registered(); - uint8_t r_blind[32]; - octra::random_bytes(r_blind, 32); - std::string enc_amount = octra::encrypt_stealth_amount(shared, (uint64_t)raw, r_blind); - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct_delta = g_pvac.encrypt((uint64_t)raw, seed); - std::string delta_cipher_str = g_pvac.encode_cipher(ct_delta); - auto commitment = g_pvac.commit_ct(ct_delta); - std::string commitment_b64 = octra::base64_encode(commitment.data(), 32); - - steps.push_back("[5/7] range proofs (parallel) - Bulletproofs R1CS"); - pvac_cipher current_ct = g_pvac.decode_cipher(eb.cipher); - pvac_cipher new_ct = g_pvac.ct_sub(current_ct, ct_delta); - uint64_t new_val = (uint64_t)(eb.decrypted - raw); - - pvac_range_proof rp_delta = nullptr; - pvac_range_proof rp_bal = nullptr; - - std::thread t_rp_delta([&]() { - rp_delta = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), ct_delta, (uint64_t)raw); - }); - std::thread t_rp_bal([&]() { - rp_bal = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), new_ct, new_val); - }); - t_rp_delta.join(); - t_rp_bal.join(); - - steps.push_back("[6/7] encoding proofs"); - std::string rp_delta_str = g_pvac.encode_range_proof(rp_delta); - std::string rp_bal_str = g_pvac.encode_range_proof(rp_bal); - g_pvac.free_range_proof(rp_delta); - g_pvac.free_range_proof(rp_bal); - g_pvac.free_cipher(ct_delta); - g_pvac.free_cipher(current_ct); - g_pvac.free_cipher(new_ct); - - steps.push_back("[7/8] Pedersen commitment + AES-GCM envelope"); - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_blind); - std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - - steps.push_back("[8/8] building stealth transaction"); - json stealth_data; - stealth_data["version"] = 5; - stealth_data["delta_cipher"] = delta_cipher_str; - stealth_data["commitment"] = commitment_b64; - stealth_data["range_proof_delta"] = rp_delta_str; - stealth_data["range_proof_balance"] = rp_bal_str; - stealth_data["eph_pub"] = octra::base64_encode(eph_pk, 32); - stealth_data["stealth_tag"] = octra::hex_encode(stag.data(), 16); - stealth_data["enc_amount"] = enc_amount; - stealth_data["claim_pub"] = octra::hex_encode(claim_pub.data(), 32); - stealth_data["amount_commitment"] = amt_commit_b64; - - auto bi = get_nonce_balance(); int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = "stealth"; - tx.amount = "0"; - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "5000"); - tx.timestamp = now_ts(); - tx.op_type = "stealth"; - tx.encrypted_data = stealth_data.dump(); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) res.status = 500; - result["steps"] = steps; - res.set_content(result.dump(), "application/json"); - - } catch (const std::exception& e) { - fprintf(stderr, "[stealth/send] exception: %s\n", e.what()); - res.status = 500; - res.set_content(err_json(std::string("stealth send failed: ") + e.what()).dump(), "application/json"); - } catch (...) { - fprintf(stderr, "[stealth/send] unknown exception\n"); - res.status = 500; - res.set_content(err_json("stealth send failed: unknown error").dump(), "application/json"); - } - }); - - svr.Get("/api/stealth/scan", [](const httplib::Request&, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); - auto r = g_rpc.get_stealth_outputs(0); - json outputs = json::array(); - if (!r.ok || !r.result.is_object() || !r.result.contains("outputs")) { - json j; - j["outputs"] = outputs; - res.set_content(j.dump(), "application/json"); - return; - } - for (auto& out : r.result["outputs"]) { - if (out.value("claimed", 0) != 0) continue; - try { - std::string eph_b64 = out["eph_pub"].get(); - auto eph_raw = octra::base64_decode(eph_b64); - if (eph_raw.size() != 32) continue; - auto shared = octra::ecdh_shared_secret(view_sk, eph_raw.data()); - auto my_tag = octra::compute_stealth_tag(shared); - std::string my_tag_hex = octra::hex_encode(my_tag.data(), 16); - if (my_tag_hex != out.value("stealth_tag", "")) continue; - auto dec = octra::decrypt_stealth_amount(shared, out.value("enc_amount", "")); - if (!dec.has_value()) continue; - auto cs = octra::compute_claim_secret(shared); - json o; - o["id"] = out.value("id", 0); - o["amount_raw"] = std::to_string(dec->amount); - o["epoch"] = out.value("epoch_id", 0); - o["sender"] = out.value("sender_addr", ""); - o["tx_hash"] = out.value("tx_hash", ""); - o["claim_secret"] = octra::hex_encode(cs.data(), 32); - o["blinding"] = octra::base64_encode(dec->blinding.data(), 32); - o["claimed"] = false; - outputs.push_back(o); - } catch (...) { - continue; - } - } - json j; - j["outputs"] = outputs; - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/stealth/claim", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - if (!body.contains("ids") || !body["ids"].is_array() || body["ids"].empty()) { - res.status = 400; - res.set_content(err_json("ids required").dump(), "application/json"); - return; - } - - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); - auto sr = g_rpc.get_stealth_outputs(0); - if (!sr.ok || !sr.result.is_object()) { - res.status = 500; - res.set_content(err_json("failed to fetch outputs").dump(), "application/json"); - return; - } - - ensure_pvac_registered(); - - std::vector req_ids; - for (auto& id : body["ids"]) { - if (id.is_string()) req_ids.push_back(id.get()); - else req_ids.push_back(std::to_string(id.get())); - } - - auto bi = get_nonce_balance(); int nonce = bi.nonce; - json results = json::array(); - - for (auto& out : sr.result["outputs"]) { - std::string out_id = out.contains("id") ? - (out["id"].is_string() ? out["id"].get() : std::to_string(out["id"].get())) : ""; - bool wanted = false; - for (auto& rid : req_ids) { - if (rid == out_id) { wanted = true; break; } - } - if (!wanted) continue; - if (out.value("claimed", 0) != 0) { - results.push_back({{"id", out_id}, {"ok", false}, {"error", "already claimed"}}); - continue; - } - try { - auto eph_raw = octra::base64_decode(out["eph_pub"].get()); - if (eph_raw.size() != 32) throw std::runtime_error("bad eph_pub"); - auto shared = octra::ecdh_shared_secret(view_sk, eph_raw.data()); - auto dec = octra::decrypt_stealth_amount(shared, out.value("enc_amount", "")); - if (!dec.has_value()) throw std::runtime_error("decrypt failed"); - auto cs = octra::compute_claim_secret(shared); - - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct_claim = g_pvac.encrypt(dec->amount, seed); - std::string claim_cipher_str = g_pvac.encode_cipher(ct_claim); - auto commit = g_pvac.commit_ct(ct_claim); - std::string commit_b64 = octra::base64_encode(commit.data(), 32); - pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct_claim, dec->amount, dec->blinding.data()); - std::string zp_str = g_pvac.encode_zero_proof(zkp); - g_pvac.free_cipher(ct_claim); - g_pvac.free_zero_proof(zkp); - - json claim_data; - claim_data["version"] = 5; - claim_data["output_id"] = out["id"]; - claim_data["claim_cipher"] = claim_cipher_str; - claim_data["commitment"] = commit_b64; - claim_data["claim_secret"] = octra::hex_encode(cs.data(), 32); - claim_data["zero_proof"] = zp_str; - - nonce++; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = g_wallet.addr; - tx.amount = "0"; - tx.nonce = nonce; - tx.ou = parse_ou(body, "3000"); - tx.timestamp = now_ts(); - tx.op_type = "claim"; - tx.encrypted_data = claim_data.dump(); - sign_tx_fields(tx); - auto sr2 = submit_tx(tx); - if (sr2.contains("error")) { - results.push_back({{"id", out_id}, {"ok", false}, {"error", sr2["error"]}}); - } else { - results.push_back({{"id", out_id}, {"ok", true}, {"tx_hash", sr2.value("tx_hash", "")}}); - } - } catch (const std::exception& e) { - results.push_back({{"id", out_id}, {"ok", false}, {"error", e.what()}}); - } - } - json j; - j["results"] = results; - res.set_content(j.dump(), "application/json"); - }); - - svr.Get("/api/tx", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::string hash = req.get_param_value("hash"); - if (hash.empty()) { - res.status = 400; - res.set_content(err_json("hash required").dump(), "application/json"); - return; - } - auto r = g_rpc.get_transaction(hash); - if (!r.ok) { - res.status = 404; - res.set_content(err_json("transaction not found").dump(), "application/json"); - return; - } - auto& t = r.result; - json j; - j["hash"] = t.value("tx_hash", hash); - j["from"] = t.value("from", ""); - j["to_"] = t.value("to", t.value("to_", "")); - j["amount_raw"] = t.value("amount_raw", t.value("amount", "0")); - j["op_type"] = t.value("op_type", "standard"); - double ts = 0.0; - if (t.contains("timestamp") && t["timestamp"].is_number()) - ts = t["timestamp"].get(); - else if (t.contains("rejected_at") && t["rejected_at"].is_number()) - ts = t["rejected_at"].get(); - j["timestamp"] = ts; - j["nonce"] = t.value("nonce", 0); - j["signature"] = t.value("signature", ""); - j["public_key"] = t.value("public_key", ""); - if (t.contains("message") && t["message"].is_string() && !t["message"].get().empty()) - j["message"] = t["message"]; - if (t.contains("encrypted_data") && t["encrypted_data"].is_string() && !t["encrypted_data"].get().empty()) - j["encrypted_data"] = t["encrypted_data"]; - if (t.contains("ou")) j["ou"] = t.value("ou", ""); - j["status"] = t.value("status", "pending"); - if (t.contains("epoch")) j["epoch"] = t["epoch"]; - else if (t.contains("epoch_id")) j["epoch"] = t["epoch_id"]; - if (t.contains("block_height")) j["block_height"] = t["block_height"]; - if (t.contains("error") && t["error"].is_object()) { - j["reject_reason"] = t["error"].value("reason", ""); - j["reject_type"] = t["error"].value("type", ""); - } - res.set_content(j.dump(), "application/json"); - }); - - svr.Get("/api/keys", [](const httplib::Request&, httplib::Response& res) { - WALLET_GUARD - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); - json j; - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["view_pubkey"] = octra::base64_encode(view_pk, 32); - j["has_master_seed"] = g_wallet.has_master_seed(); - octra::secure_zero(view_sk, 32); - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/keys/private", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string pin = body.value("pin", ""); - try { octra::load_wallet_encrypted(g_wallet_path, pin); } catch (...) { - res.status = 403; - res.set_content(err_json("wrong PIN").dump(), "application/json"); - return; - } - json j; - j["private_key"] = g_wallet.priv_b64; - j["mnemonic"] = g_wallet.mnemonic; - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/contract/compile", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string source = body.value("source", ""); - if (source.empty()) { - res.status = 400; - res.set_content(err_json("source required").dump(), "application/json"); - return; - } - auto r = g_rpc.compile_assembly(source); - if (!r.ok) { - res.status = 400; - res.set_content(err_json(r.error).dump(), "application/json"); - return; - } - json j; - j["bytecode"] = r.result.value("bytecode", ""); - j["size"] = r.result.value("size", 0); - j["instructions"] = r.result.value("instructions", 0); - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/contract/compile-aml", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - try { - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string source = body.value("source", ""); - if (source.empty()) { - res.status = 400; - res.set_content(err_json("source required").dump(), "application/json"); - return; - } - auto r = g_rpc.compile_aml(source); - if (!r.ok) { - res.status = 400; - std::string safe_err = r.error; - for (auto& ch : safe_err) { if ((unsigned char)ch > 127) ch = '?'; } - res.set_content(err_json(safe_err).dump(), "application/json"); - return; - } - json j; - j["bytecode"] = r.result.value("bytecode", ""); - j["size"] = r.result.value("size", 0); - j["instructions"] = r.result.value("instructions", 0); - j["version"] = r.result.value("version", ""); - if (r.result.contains("abi")) j["abi"] = r.result["abi"]; - if (r.result.contains("disasm")) j["disasm"] = r.result["disasm"]; - res.set_content(j.dump(), "application/json"); - } catch (const std::exception& ex) { - res.status = 500; - res.set_content(err_json(std::string("internal error: ") + ex.what()).dump(), "application/json"); - } catch (...) { - res.status = 500; - res.set_content(err_json("internal error").dump(), "application/json"); - } - }); - - svr.Post("/api/contract/compile-project", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - try { - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - auto files = body.value("files", json::array()); - std::string main_path = body.value("main", "main.aml"); - if (files.empty()) { - res.status = 400; - res.set_content(err_json("files required").dump(), "application/json"); - return; - } - auto r = g_rpc.compile_aml_multi(files, main_path); - if (!r.ok) { - res.status = 400; - std::string safe_err = r.error; - for (auto& ch : safe_err) { if ((unsigned char)ch > 127) ch = '?'; } - res.set_content(err_json(safe_err).dump(), "application/json"); - return; - } - json j; - j["bytecode"] = r.result.value("bytecode", ""); - j["size"] = r.result.value("size", 0); - j["instructions"] = r.result.value("instructions", 0); - j["version"] = r.result.value("version", ""); - if (r.result.contains("abi")) j["abi"] = r.result["abi"]; - if (r.result.contains("disasm")) j["disasm"] = r.result["disasm"]; - res.set_content(j.dump(), "application/json"); - } catch (const std::exception& ex) { - res.status = 500; - res.set_content(err_json(std::string("internal error: ") + ex.what()).dump(), "application/json"); - } catch (...) { - res.status = 500; - res.set_content(err_json("internal error").dump(), "application/json"); - } - }); - - svr.Post("/api/contract/address", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string bytecode = body.value("bytecode", ""); - if (bytecode.empty()) { - res.status = 400; - res.set_content(err_json("bytecode required").dump(), "application/json"); - return; - } - int nonce_val = 0; - auto bi = get_nonce_balance(); - nonce_val = bi.nonce + 1; - auto r = g_rpc.compute_contract_address(bytecode, g_wallet.addr, nonce_val); - if (!r.ok) { - res.status = 400; - res.set_content(err_json(r.error).dump(), "application/json"); - return; - } - json j; - j["address"] = r.result.value("address", ""); - j["deployer"] = r.result.value("deployer", ""); - j["nonce"] = r.result.value("nonce", 0); - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/contract/deploy", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string bytecode = body.value("bytecode", ""); - if (bytecode.empty()) { - res.status = 400; - res.set_content(err_json("bytecode required").dump(), "application/json"); - return; - } - auto bi = get_nonce_balance(); - int nonce = bi.nonce; - auto ar = g_rpc.compute_contract_address(bytecode, g_wallet.addr, nonce + 1); - if (!ar.ok) { - res.status = 400; - res.set_content(err_json(ar.error).dump(), "application/json"); - return; - } - std::string contract_addr = ar.result.value("address", ""); - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = contract_addr; - tx.amount = "0"; - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "50000000"); - tx.timestamp = now_ts(); - tx.op_type = "deploy"; - tx.encrypted_data = bytecode; - std::string params_str = body.value("params", ""); - if (!params_str.empty()) tx.message = params_str; - std::string source_text = body.value("source", ""); - std::string abi_text = body.value("abi", ""); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) { - res.status = 500; - } else { - result["contract_address"] = contract_addr; - } - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/contract/verify", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - try { - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string addr = body.value("address", ""); - std::string source = body.value("source", ""); - if (addr.empty() || source.empty()) { - res.status = 400; - res.set_content(err_json("address and source required").dump(), "application/json"); - return; - } - nlohmann::json verify_params = nlohmann::json::array({addr, source}); - if (body.contains("files") && body["files"].is_array()) { - verify_params.push_back(body["files"]); - } - auto r = g_rpc.call("contract_verify", verify_params, 15); - if (!r.ok) { - res.status = 400; - std::string safe_err = r.error; - for (auto& ch : safe_err) { if ((unsigned char)ch > 127) ch = '?'; } - res.set_content(err_json(safe_err).dump(), "application/json"); - return; - } - res.set_content(r.result.dump(), "application/json"); - } catch (const std::exception& ex) { - res.status = 500; - res.set_content(err_json(std::string("internal error: ") + ex.what()).dump(), "application/json"); - } catch (...) { - res.status = 500; - res.set_content(err_json("internal error").dump(), "application/json"); - } - }); - - svr.Post("/api/contract/call", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string addr = body.value("address", ""); - std::string method = body.value("method", ""); - if (addr.empty() || method.empty()) { - res.status = 400; - res.set_content(err_json("address and method required").dump(), "application/json"); - return; - } - std::string params_str = "[]"; - if (body.contains("params")) params_str = body["params"].dump(); - std::string amount_str = body.value("amount", "0"); - auto bi = get_nonce_balance(); - int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = addr; - tx.amount = amount_str; - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "1000"); - tx.timestamp = now_ts(); - tx.op_type = "call"; - tx.encrypted_data = method; - tx.message = params_str; - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) res.status = 500; - res.set_content(result.dump(), "application/json"); - }); - - svr.Get("/api/contract/view", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::string addr = req.get_param_value("address"); - std::string method = req.get_param_value("method"); - if (addr.empty() || method.empty()) { - res.status = 400; - res.set_content(err_json("address and method required").dump(), "application/json"); - return; - } - std::string params_str = req.get_param_value("params"); - json params = json::array(); - if (!params_str.empty()) { - try { params = json::parse(params_str); } catch (...) {} - } - auto r = g_rpc.contract_call_view(addr, method, params, g_wallet.addr); - if (!r.ok) { - res.status = 400; - res.set_content(err_json(r.error).dump(), "application/json"); - return; - } - res.set_content(r.result.dump(), "application/json"); - }); - - svr.Post("/api/fhe/encrypt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - if (!g_pvac_ok) { - res.status = 500; - res.set_content(err_json("pvac not available").dump(), "application/json"); - return; - } - auto body = json::parse(req.body, nullptr, false); - if (body.is_discarded() || !body.contains("value")) { - res.status = 400; - res.set_content(err_json("missing value").dump(), "application/json"); - return; - } - int64_t value = body["value"].get(); - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct = g_pvac.encrypt(static_cast(value), seed); - auto data = g_pvac.serialize_cipher(ct); - std::string b64 = octra::base64_encode(data.data(), data.size()); - g_pvac.free_cipher(ct); - json result; - result["ciphertext"] = b64; - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/fhe/decrypt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - if (!g_pvac_ok) { - res.status = 500; - res.set_content(err_json("pvac not available").dump(), "application/json"); - return; - } - auto body = json::parse(req.body, nullptr, false); - if (body.is_discarded() || !body.contains("ciphertext")) { - res.status = 400; - res.set_content(err_json("missing ciphertext").dump(), "application/json"); - return; - } - std::string b64 = body["ciphertext"].get(); - auto raw = octra::base64_decode(b64); - if (raw.empty()) { - res.status = 400; - res.set_content(err_json("invalid base64").dump(), "application/json"); - return; - } - pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); - if (!ct) { - res.status = 400; - res.set_content(err_json("invalid ciphertext").dump(), "application/json"); - return; - } - uint64_t lo = 0, hi = 0; - g_pvac.decrypt_fp(ct, lo, hi); - g_pvac.free_cipher(ct); - int64_t val; - if (hi == 0) { - val = static_cast(lo); - } else { - __uint128_t p = (__uint128_t(1) << 127) - 1; - __uint128_t full = (__uint128_t(hi) << 64) | lo; - if (full > p / 2) val = -static_cast(p - full); - else val = static_cast(lo); - } - json result; - result["value"] = val; - res.set_content(result.dump(), "application/json"); - }); - - svr.Get("/api/contract/info", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::string addr = req.get_param_value("address"); - if (addr.empty()) { - res.status = 400; - res.set_content(err_json("address required").dump(), "application/json"); - return; - } - auto r = g_rpc.vm_contract(addr); - if (!r.ok) { - res.status = 404; - res.set_content(err_json(r.error).dump(), "application/json"); - return; - } - res.set_content(r.result.dump(), "application/json"); - }); - - svr.Get("/api/contract/receipt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::string hash = req.get_param_value("hash"); - if (hash.empty()) { - res.status = 400; - res.set_content(err_json("hash required").dump(), "application/json"); - return; - } - auto r = g_rpc.contract_receipt(hash); - if (!r.ok) { - res.status = 404; - res.set_content(err_json(r.error).dump(), "application/json"); - return; - } - res.set_content(r.result.dump(), "application/json"); - }); - - static json g_token_cache; - static double g_token_cache_ts = 0; - static std::string g_token_cache_addr; - - svr.Get("/api/tokens", [](const httplib::Request&, httplib::Response& res) { - WALLET_GUARD - double now = (double)time(nullptr); - if (!g_token_cache.empty() && g_token_cache_addr == g_wallet.addr - && (now - g_token_cache_ts) < 30.0) { - res.set_content(g_token_cache.dump(), "application/json"); - return; - } - auto lr = g_rpc.list_contracts(); - json tokens = json::array(); - if (lr.ok && lr.result.contains("contracts")) { - auto& contracts = lr.result["contracts"]; - for (auto& c : contracts) { - std::string addr = c.value("address", ""); - if (addr.empty()) continue; - auto sr = g_rpc.contract_storage(addr, "symbol"); - if (!sr.ok || !sr.result.contains("value") || sr.result["value"].is_null()) continue; - std::string sym = sr.result.value("value", ""); - if (sym.empty() || sym == "0") continue; - if (sym.size() > 10) sym = sym.substr(0, 10); - auto br = g_rpc.contract_call_view(addr, "balance_of", - json::array({g_wallet.addr}), g_wallet.addr); - std::string bal = (br.ok && br.result.contains("result") && !br.result["result"].is_null()) - ? br.result.value("result", "0") : "0"; - if (bal == "0" || bal.empty()) continue; - auto nr = g_rpc.contract_storage(addr, "name"); - std::string name = (nr.ok && nr.result.contains("value") && !nr.result["value"].is_null()) - ? nr.result.value("value", "") : sym; - if (name.size() > 32) name = name.substr(0, 32); - auto tr = g_rpc.contract_storage(addr, "total_supply"); - std::string supply = (tr.ok && tr.result.contains("value") && !tr.result["value"].is_null()) - ? tr.result.value("value", "0") : "0"; - auto dr = g_rpc.contract_storage(addr, "decimals"); - std::string decimals = (dr.ok && dr.result.contains("value") && !dr.result["value"].is_null()) - ? dr.result.value("value", "0") : "0"; - json tok; - tok["address"] = addr; - tok["name"] = name; - tok["symbol"] = sym; - tok["total_supply"] = supply; - tok["balance"] = bal; - tok["decimals"] = decimals; - tok["owner"] = c.value("owner", ""); - tokens.push_back(tok); - } - } - json j; - j["tokens"] = tokens; - j["count"] = tokens.size(); - j["wallet_address"] = g_wallet.addr; - g_token_cache = j; - g_token_cache_ts = now; - g_token_cache_addr = g_wallet.addr; - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/token/transfer", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string token = body.value("token", ""); - std::string to = body.value("to", ""); - std::string amount_str = body.value("amount", ""); - if (token.empty() || to.empty() || amount_str.empty()) { - res.status = 400; - res.set_content(err_json("token, to, and amount required").dump(), "application/json"); - return; - } - long long amount_val = 0; - try { amount_val = std::stoll(amount_str); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid amount").dump(), "application/json"); - return; - } - if (amount_val <= 0) { - res.status = 400; - res.set_content(err_json("amount must be positive").dump(), "application/json"); - return; - } - auto bi = get_nonce_balance(); - int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = token; - tx.amount = "0"; - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "1000"); - tx.timestamp = now_ts(); - tx.op_type = "call"; - tx.encrypted_data = "transfer"; - json params = json::array({to, amount_val}); - tx.message = params.dump(); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) res.status = 500; - res.set_content(result.dump(), "application/json"); - }); - - svr.Post("/api/settings", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string new_rpc = body.value("rpc_url", ""); - std::string new_explorer = body.value("explorer_url", ""); - if (new_rpc.empty()) { - res.status = 400; - res.set_content(err_json("rpc_url required").dump(), "application/json"); - return; - } - bool cache_cleared = false; - try { - std::string old_rpc = g_wallet.rpc_url; - if (!new_explorer.empty()) g_wallet.explorer_url = new_explorer; - octra::save_settings(g_wallet_path, g_wallet, new_rpc, g_pin); - g_rpc.set_url(g_wallet.rpc_url); - if (old_rpc != g_wallet.rpc_url) { - g_txcache.clear(); - g_txcache.put("meta:rpc_url", g_wallet.rpc_url); - cache_cleared = true; - fprintf(stderr, "txcache cleared: rpc changed %s -> %s\n", - old_rpc.c_str(), g_wallet.rpc_url.c_str()); - } - } catch (const std::exception& e) { - res.status = 500; - res.set_content(err_json(e.what()).dump(), "application/json"); - return; - } - json j; - j["ok"] = true; - j["rpc_url"] = g_wallet.rpc_url; - j["explorer_url"] = g_wallet.explorer_url; - j["cache_cleared"] = cache_cleared; - res.set_content(j.dump(), "application/json"); - }); - - svr.Post("/api/wallet/change-pin", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::lock_guard lock(g_mtx); - json body; - try { body = json::parse(req.body); } catch (...) { - res.status = 400; - res.set_content(err_json("invalid json").dump(), "application/json"); - return; - } - std::string cur_pin = body.value("current_pin", ""); - std::string new_pin = body.value("new_pin", ""); - if (cur_pin.size() != 6 || !std::all_of(cur_pin.begin(), cur_pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("current PIN must be 6 digits").dump(), "application/json"); - return; - } - if (new_pin.size() != 6 || !std::all_of(new_pin.begin(), new_pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("new PIN must be 6 digits").dump(), "application/json"); - return; - } - if (cur_pin != g_pin) { - res.status = 403; - res.set_content(err_json("wrong current PIN").dump(), "application/json"); - return; - } - try { - octra::save_wallet_encrypted(g_wallet_path, g_wallet, new_pin); - octra::secure_zero(&g_pin[0], g_pin.size()); - g_pin = new_pin; - octra::try_mlock(&g_pin[0], g_pin.size()); - fprintf(stderr, "PIN changed\n"); - } catch (const std::exception& e) { - res.status = 500; - res.set_content(err_json(e.what()).dump(), "application/json"); - return; - } - json j; - j["ok"] = true; - res.set_content(j.dump(), "application/json"); - }); - - std::thread pvac_bg([&]() { - std::this_thread::sleep_for(std::chrono::seconds(10)); - while (true) { - try { - if (g_wallet_loaded && g_pvac_ok) { - auto entries = octra::load_manifest(); - for (auto& e : entries) { - if (e.addr.empty() || e.file.empty()) continue; - auto ar = g_rpc.get_account(e.addr); - if (!ar.ok) continue; - try { - auto w = octra::load_wallet_encrypted(e.file, g_pin); - ensure_pubkey_registered(w.addr, w.sk, w.pub_b64); - auto pr = g_rpc.get_pvac_pubkey(e.addr); - bool pvac_ok = pr.ok && pr.result.is_object() && !pr.result["pvac_pubkey"].is_null() - && pr.result["pvac_pubkey"].is_string() && !pr.result["pvac_pubkey"].get().empty(); - if (!pvac_ok) { - octra::PvacBridge tmp_pvac; - if (tmp_pvac.init(w.priv_b64)) { - auto pk_raw = tmp_pvac.serialize_pubkey(); - std::string pk_blob(pk_raw.begin(), pk_raw.end()); - std::string pk_b64 = tmp_pvac.serialize_pubkey_b64(); - std::string reg_sig = octra::sign_register_request(w.addr, pk_blob, w.sk); - std::string kat = compute_aes_kat_hex(); - auto rr = g_rpc.register_pvac_pubkey(w.addr, pk_b64, reg_sig, w.pub_b64, kat); - if (rr.ok) fprintf(stderr, "[bg] pvac registered %s\n", w.addr.c_str()); - else fprintf(stderr, "[bg] pvac failed %s: %s\n", w.addr.c_str(), rr.error.c_str()); - } - } - octra::secure_zero(w.sk, 64); - } catch (...) {} - } - } - } catch (...) {} - std::this_thread::sleep_for(std::chrono::seconds(60)); - } - }); - pvac_bg.detach(); - printf("octra_wallet listening on http://127.0.0.1:%d\n", port); svr.listen("127.0.0.1", port); return 0; -} \ No newline at end of file +} diff --git a/rpc_client.hpp b/rpc_client.hpp index c0fbba3..4305d06 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -76,7 +76,8 @@ class RpcClient { } public: - RpcClient() : path_("/rpc"), ssl_(true), port_(443) {} + // Default constructor uses the standard Octra RPC endpoint + RpcClient() : host_("rpc.octra.io"), path_("/rpc"), ssl_(true), port_(443) {} explicit RpcClient(const std::string& url) { parse_url(url); } void set_url(const std::string& url) { parse_url(url); } @@ -173,8 +174,7 @@ class RpcClient { } - // rpc compl - + // Compile AML with multiple source files (multi-file project support) RpcResult compile_aml_multi(const nlohmann::json& files, const std::string& main_path) { nlohmann::json payload; payload["files"] = files; diff --git a/wallet.hpp b/wallet.hpp index e269fcd..30e685a 100644 --- a/wallet.hpp +++ b/wallet.hpp @@ -100,7 +100,17 @@ inline void ensure_data_dir() { } inline std::string wallet_path_for(const std::string& addr) { - std::string prefix = addr.size() > 11 ? addr.substr(3, 8) : "unknown"; + // Require at least "oct" prefix + 8 chars for the file prefix slice + // Also sanitize: only allow alphanumeric chars to prevent path traversal + std::string prefix = "unknown"; + if (addr.size() > 11 && addr.substr(0, 3) == "oct") { + std::string raw = addr.substr(3, 8); + bool safe = true; + for (char c : raw) { + if (!isalnum((unsigned char)c)) { safe = false; break; } + } + if (safe) prefix = raw; + } return std::string(WALLET_DIR) + "/wallet_" + prefix + ".oct"; } @@ -553,4 +563,4 @@ inline std::vector scan_and_merge_oct_files() { #endif return entries; } -} \ No newline at end of file +}