ESP32: добавить USB-диагностику регистрации Solana

This commit is contained in:
AidarKC 2026-06-13 00:01:57 +04:00
parent 21030b1d51
commit 436e1f0c53
3 changed files with 293 additions and 31 deletions

View File

@ -0,0 +1,45 @@
# ESP32 USB-диагностика регистрации Solana
- статус: `pending`
## Что сделано
- В основной скетч `shine_homeserver_main` добавено сохранение последней диагностики регистрации Solana в `Preferences`.
- Добавлены USB-команды через `Serial`:
- `last_error`
- `last_diag`
- `reg_diag`
- `clear_error`
- `clear_diag`
- `help`
- Перед отправкой `create_user_pda` добавлена RPC-проверка `users_economy_config_pda`.
- Стартовый `paid_limit_bytes` в подписываемой записи теперь берётся из on-chain `users_economy_config`, а не из хардкода.
## Что проверять
1. Подключить устройство по USB.
2. Открыть последовательный порт `115200`.
3. Отправить команду `last_error`.
4. Убедиться, что устройство печатает сохранённую диагностику между маркерами:
- `LAST_REGISTER_DIAG_BEGIN`
- `LAST_REGISTER_DIAG_END`
5. Запустить регистрацию с устройства и дождаться ошибки или успеха.
6. Снова отправить `last_error`.
7. Проверить, что в диагностике есть:
- `status`
- `summary`
- `rpc`
- `user_pda`
- `users_economy_config_pda`
- `inflow_vault_pda`
- `root_pub`
- `blockchain_pub`
- `device_pub`
8. При ошибке `0x3` проверить, что текст стал конкретнее и помогает понять, какая PDA или RPC-конфигурация не совпала.
## Ожидаемый результат
- Последняя ошибка регистрации читается по USB без просмотра экрана.
- После неудачной регистрации на устройстве остаётся подробная диагностическая запись.
- Если `users_economy_config_pda` отсутствует или принадлежит не той программе, это явно видно до отправки транзакции.

View File

@ -256,6 +256,11 @@ static bool gRegisterConfirmCanSubmit = false;
static String gRegisterResultMessage;
static String gRegisterResultDetails;
static bool gRegisterResultSuccess = false;
static String gLastRegisterDiagStatus = "none";
static String gLastRegisterDiagSummary;
static String gLastRegisterDiagDetails;
static String gLastRegisterDiagTime;
static String gSerialCommandBuffer;
static String gShineSessionId;
static String gShineSessionKey;
static String gShineStoragePwd;
@ -362,6 +367,12 @@ static bool rpcCallSolana(const char *method, const String &paramsJson, String &
static bool rpcResponseHasError(const String &payload);
static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut);
static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut);
static bool loadUsersEconomyConfigState(const String &economyConfigAddress,
uint64_t &registrationFeeLamportsOut,
uint64_t &lamportsPerLimitStepOut,
uint64_t &startBonusLimitOut,
String &messageOut);
static bool loadAccountOwner(const String &address, String &ownerOut, bool &existsOut, String &messageOut);
static bool extractRpcErrorSummary(const String &payload, String &messageOut);
static String compactRpcLogs(const String &payload, int maxLines = 3);
static bool simulateTransactionForError(const String &txBase64, String &messageOut);
@ -374,6 +385,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
const uint8_t devicePub[32],
const uint8_t blockchainPub[32],
const uint8_t lastBlockSignature[64],
uint64_t paidLimitBytes,
uint64_t createdAtMs);
static std::vector<uint8_t> buildCreateInstructionData(
const String &login,
@ -399,6 +411,10 @@ static bool signMessageEd25519(const std::vector<uint8_t> &message, const uint8_
static String encodeTransactionBase64(const uint8_t signature[64], const std::vector<uint8_t> &message);
static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut);
static bool registerHomeserverOnSolana(String &messageOut);
static void saveRegisterDiag(const String &status, const String &summary, const String &details);
static void printRegisterDiagToSerial();
static void clearRegisterDiag();
static void handleUsbSerialCommands();
static void prepareRegisterAccountScreen();
static String buildSessionKeyStringFromPublicBase64(const String &pubB64);
static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]);
@ -1140,6 +1156,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
const uint8_t devicePub[32],
const uint8_t blockchainPub[32],
const uint8_t lastBlockSignature[64],
uint64_t paidLimitBytes,
uint64_t createdAtMs) {
std::vector<uint8_t> out;
out.reserve(512);
@ -1169,7 +1186,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
out.push_back(1);
pushStrU8(out, blockchainName);
pushFixed(out, blockchainPub, 32);
pushU64LE(out, 100000);
pushU64LE(out, paidLimitBytes);
pushU64LE(out, 0);
pushU32LE(out, 0);
out.insert(out.end(), 32, 0);
@ -1306,6 +1323,82 @@ static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &me
return false;
}
static bool loadAccountOwner(const String &address, String &ownerOut, bool &existsOut, String &messageOut) {
ownerOut = "";
existsOut = false;
String payload;
if (!rpcCallSolana("getAccountInfo", "[\"" + address + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) {
messageOut = "Failed to read account info";
return false;
}
if (payload.indexOf("\"value\":null") >= 0) {
existsOut = false;
return true;
}
existsOut = true;
if (!jsonStringField(payload, "owner", ownerOut) || ownerOut.isEmpty()) {
messageOut = "Account owner missing in RPC response";
return false;
}
return true;
}
static bool loadUsersEconomyConfigState(const String &economyConfigAddress,
uint64_t &registrationFeeLamportsOut,
uint64_t &lamportsPerLimitStepOut,
uint64_t &startBonusLimitOut,
String &messageOut) {
registrationFeeLamportsOut = 0;
lamportsPerLimitStepOut = 0;
startBonusLimitOut = 0;
String payload;
if (!rpcCallSolana("getAccountInfo", "[\"" + economyConfigAddress + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) {
messageOut = "Failed to read users economy config";
return false;
}
if (payload.indexOf("\"value\":null") >= 0) {
messageOut = "Users economy config is missing on this Solana RPC";
return false;
}
String owner;
if (!jsonStringField(payload, "owner", owner) || owner.isEmpty()) {
messageOut = "Users economy config owner is missing";
return false;
}
if (owner != String(kShineUsersProgramId)) {
messageOut = String("Users economy config owner mismatch: ") + owner;
return false;
}
String dataB64;
if (!jsonStringField(payload, "data", dataB64) || dataB64.isEmpty()) {
messageOut = "Users economy config base64 is missing";
return false;
}
std::vector<uint8_t> raw;
if (!base64DecodeStd(dataB64, raw)) {
messageOut = "Users economy config base64 decode failed";
return false;
}
if (raw.size() < 25) {
messageOut = "Users economy config is too short";
return false;
}
registrationFeeLamportsOut = 0;
lamportsPerLimitStepOut = 0;
startBonusLimitOut = 0;
for (int i = 0; i < 8; ++i) {
registrationFeeLamportsOut |= ((uint64_t)raw[1 + i]) << (8 * i);
lamportsPerLimitStepOut |= ((uint64_t)raw[9 + i]) << (8 * i);
startBonusLimitOut |= ((uint64_t)raw[17 + i]) << (8 * i);
}
return true;
}
static String compactRpcLogs(const String &payload, int maxLines) {
String out;
int pos = payload.indexOf("\"logs\"");
@ -1490,22 +1583,33 @@ static bool awaitTransactionConfirmation(const String &signatureB58, String &mes
static bool registerHomeserverOnSolana(String &messageOut) {
messageOut = "";
String cleanLogin = normalizeLoginValue(gLoginValue);
if (cleanLogin.isEmpty()) {
messageOut = "Login is not set";
String diagDetails;
auto failWithDiag = [&](const String &summary) -> bool {
messageOut = summary;
saveRegisterDiag("error", summary, diagDetails);
printRegisterDiagToSerial();
return false;
};
String cleanLogin = normalizeLoginValue(gLoginValue);
diagDetails += String("login=") + cleanLogin + "\n";
diagDetails += String("rpc=") + gSolanaRpcUrl + "\n";
diagDetails += String("shine_server=") + gShineServerUrl + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
if (cleanLogin.isEmpty()) {
return failWithDiag("Login is not set");
}
if (!gSecretConfigured || gRootPrivB58.isEmpty() || gBlockchainPrivB58.isEmpty() || gDevicePrivB58.isEmpty()) {
messageOut = "Secret is not ready";
return false;
return failWithDiag("Secret is not ready");
}
if (WiFi.status() != WL_CONNECTED) {
messageOut = "Connect Wi-Fi first";
return false;
diagDetails += "wifi=disconnected\n";
return failWithDiag("Connect Wi-Fi first");
}
diagDetails += String("wifi=connected:") + WiFi.localIP().toString() + "\n";
if (gSolanaRpcUrl.isEmpty()) {
messageOut = "Set Solana RPC first";
return false;
return failWithDiag("Set Solana RPC first");
}
String existingPda;
@ -1520,11 +1624,13 @@ static bool registerHomeserverOnSolana(String &messageOut) {
gAccountStatusMessage = "User is already registered";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered";
refreshAccountPdaStatus();
diagDetails += String("user_pda=") + existingPda + "\n";
saveRegisterDiag("ok", "User is already registered", diagDetails);
return true;
}
if (pdaCheckMessage == "Failed to derive user PDA" || pdaCheckMessage == "Failed to check PDA" || pdaCheckMessage == "Unexpected getAccountInfo response") {
messageOut = pdaCheckMessage;
return false;
diagDetails += String("user_pda_check=") + pdaCheckMessage + "\n";
return failWithDiag(pdaCheckMessage);
}
uint8_t userPda[32];
@ -1540,10 +1646,44 @@ static bool registerHomeserverOnSolana(String &messageOut) {
!findProgramAddress({
std::vector<uint8_t>((const uint8_t *)kPaymentsInflowSeed, (const uint8_t *)kPaymentsInflowSeed + strlen(kPaymentsInflowSeed))
}, kShinePaymentsProgramId, inflowVault)) {
messageOut = "Failed to derive required PDAs";
return false;
return failWithDiag("Failed to derive required PDAs");
}
String userPdaB58 = bytesToBase58(userPda, 32);
String economyConfigB58 = bytesToBase58(economyConfig, 32);
String inflowVaultB58 = bytesToBase58(inflowVault, 32);
diagDetails += String("user_pda=") + userPdaB58 + "\n";
diagDetails += String("users_economy_config_pda=") + economyConfigB58 + "\n";
diagDetails += String("inflow_vault_pda=") + inflowVaultB58 + "\n";
String accountInfoMessage;
String ownerValue;
bool ownerExists = false;
if (!loadAccountOwner(userPdaB58, ownerValue, ownerExists, accountInfoMessage)) {
diagDetails += String("user_pda_rpc_error=") + accountInfoMessage + "\n";
return failWithDiag(accountInfoMessage);
}
diagDetails += String("user_pda_exists=") + (ownerExists ? "true" : "false") + "\n";
if (ownerExists) {
diagDetails += String("user_pda_owner=") + ownerValue + "\n";
return failWithDiag("User PDA already exists on RPC");
}
uint64_t registrationFeeLamports = 0;
uint64_t lamportsPerLimitStep = 0;
uint64_t startBonusLimit = 0;
if (!loadUsersEconomyConfigState(economyConfigB58,
registrationFeeLamports,
lamportsPerLimitStep,
startBonusLimit,
accountInfoMessage)) {
diagDetails += String("users_economy_config_error=") + accountInfoMessage + "\n";
return failWithDiag(accountInfoMessage);
}
diagDetails += String("registration_fee_lamports=") + String((unsigned long long)registrationFeeLamports) + "\n";
diagDetails += String("lamports_per_limit_step=") + String((unsigned long long)lamportsPerLimitStep) + "\n";
diagDetails += String("start_bonus_limit=") + String((unsigned long long)startBonusLimit) + "\n";
uint8_t rootSeed[32] = {};
uint8_t rootPub[32] = {};
uint8_t rootSec[64] = {};
@ -1556,9 +1696,11 @@ static bool registerHomeserverOnSolana(String &messageOut) {
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) ||
!deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) ||
!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) {
messageOut = "Failed to restore keys";
return false;
return failWithDiag("Failed to restore keys");
}
diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n";
diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n";
diagDetails += String("device_pub=") + bytesToBase58(devicePub, 32) + "\n";
String blockchainName = cleanLogin + "-001";
std::vector<uint8_t> lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName);
@ -1566,21 +1708,19 @@ static bool registerHomeserverOnSolana(String &messageOut) {
uint8_t lastBlockSignature[64];
sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash);
if (!signMessageEd25519(std::vector<uint8_t>(lastBlockHash, lastBlockHash + 32), blockchainSec, lastBlockSignature)) {
messageOut = "Failed to sign LastBlockState";
return false;
return failWithDiag("Failed to sign LastBlockState");
}
uint64_t createdAtMs = shineNowMs();
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
cleanLogin, blockchainName, gShineServerUrl,
rootPub, devicePub, blockchainPub,
lastBlockSignature, createdAtMs);
lastBlockSignature, startBonusLimit, createdAtMs);
uint8_t unsignedHash[32];
uint8_t rootSignature[64];
sha256calc(unsignedRecord.data(), unsignedRecord.size(), unsignedHash);
if (!signMessageEd25519(std::vector<uint8_t>(unsignedHash, unsignedHash + 32), rootSec, rootSignature)) {
messageOut = "Failed to sign PDA record";
return false;
return failWithDiag("Failed to sign PDA record");
}
std::vector<uint8_t> createData = buildCreateInstructionData(
@ -1593,8 +1733,10 @@ static bool registerHomeserverOnSolana(String &messageOut) {
uint8_t recentBlockhash[32];
String recentBlockhash58;
if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) {
return false;
diagDetails += String("blockhash_error=") + messageOut + "\n";
return failWithDiag(messageOut);
}
diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n";
std::vector<uint8_t> message = buildLegacyMessage(
recentBlockhash,
@ -1607,16 +1749,16 @@ static bool registerHomeserverOnSolana(String &messageOut) {
createData);
uint8_t txSignature[64];
if (!signMessageEd25519(message, deviceSec, txSignature)) {
messageOut = "Failed to sign Solana transaction";
return false;
return failWithDiag("Failed to sign Solana transaction");
}
String txBase64 = encodeTransactionBase64(txSignature, message);
String signatureB58 = bytesToBase58(txSignature, 64);
diagDetails += String("tx_signature=") + signatureB58 + "\n";
String payload;
if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) {
messageOut = "RPC did not accept transaction";
return false;
diagDetails += "send_transaction_rpc_error=true\n";
return failWithDiag("RPC did not accept transaction");
}
if (rpcResponseHasError(payload)) {
if (!extractRpcErrorSummary(payload, messageOut)) {
@ -1630,13 +1772,15 @@ static bool registerHomeserverOnSolana(String &messageOut) {
messageOut += " | simulate: " + simulated;
}
}
return false;
diagDetails += String("send_transaction_error=") + messageOut + "\n";
return failWithDiag(messageOut);
}
if (!awaitTransactionConfirmation(signatureB58, messageOut)) {
return false;
diagDetails += String("confirmation_error=") + messageOut + "\n";
return failWithDiag(messageOut);
}
gUserPdaAddress = bytesToBase58(userPda, 32);
gUserPdaAddress = userPdaB58;
gRegistrationSignature = signatureB58;
gAccountStatusMessage = "Solana registration complete";
gAccountPdaStatus = ACCOUNT_PDA_OK;
@ -1646,6 +1790,8 @@ static bool registerHomeserverOnSolana(String &messageOut) {
saveAccountPrefs();
refreshAccountPdaStatus();
messageOut = "Solana registration confirmed";
saveRegisterDiag("ok", messageOut, diagDetails);
printRegisterDiagToSerial();
return true;
}
@ -2573,6 +2719,10 @@ static void loadPrefs() {
gShineStoragePwd = gPrefs.getString("shine_store_pwd", "");
gUserPdaAddress = gPrefs.getString("user_pda", "");
gRegistrationSignature = gPrefs.getString("registration_sig", "");
gLastRegisterDiagStatus = gPrefs.getString("reg_diag_status", "none");
gLastRegisterDiagSummary = gPrefs.getString("reg_diag_summary", "");
gLastRegisterDiagDetails = gPrefs.getString("reg_diag_details", "");
gLastRegisterDiagTime = gPrefs.getString("reg_diag_time", "");
gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load";
gAccountCheckPending = true;
gLastAccountCheckMs = 0;
@ -2687,6 +2837,71 @@ static String wifiHomeSummary() {
return String("Wi-Fi (") + gWifiSavedSsid + ") disconnected";
}
static void saveRegisterDiag(const String &status, const String &summary, const String &details) {
gLastRegisterDiagStatus = status;
gLastRegisterDiagSummary = summary.length() > 240 ? summary.substring(0, 240) : summary;
gLastRegisterDiagDetails = details.length() > 1800 ? details.substring(0, 1800) : details;
gLastRegisterDiagTime = String(shineNowMs());
gPrefs.putString("reg_diag_status", gLastRegisterDiagStatus);
gPrefs.putString("reg_diag_summary", gLastRegisterDiagSummary);
gPrefs.putString("reg_diag_details", gLastRegisterDiagDetails);
gPrefs.putString("reg_diag_time", gLastRegisterDiagTime);
}
static void clearRegisterDiag() {
gLastRegisterDiagStatus = "none";
gLastRegisterDiagSummary = "";
gLastRegisterDiagDetails = "";
gLastRegisterDiagTime = "";
gPrefs.remove("reg_diag_status");
gPrefs.remove("reg_diag_summary");
gPrefs.remove("reg_diag_details");
gPrefs.remove("reg_diag_time");
}
static void printRegisterDiagToSerial() {
Serial.println("LAST_REGISTER_DIAG_BEGIN");
Serial.println(String("status=") + gLastRegisterDiagStatus);
Serial.println(String("time_ms=") + gLastRegisterDiagTime);
Serial.println(String("summary=") + gLastRegisterDiagSummary);
Serial.println("details<<");
Serial.println(gLastRegisterDiagDetails);
Serial.println(">>details");
Serial.println("LAST_REGISTER_DIAG_END");
}
static void handleUsbSerialCommands() {
while (Serial.available() > 0) {
char ch = (char)Serial.read();
if (ch == '\r') {
continue;
}
if (ch == '\n') {
String cmd = gSerialCommandBuffer;
gSerialCommandBuffer = "";
cmd.trim();
cmd.toLowerCase();
if (cmd.isEmpty()) {
continue;
}
if (cmd == "last_error" || cmd == "last_diag" || cmd == "reg_diag") {
printRegisterDiagToSerial();
} else if (cmd == "clear_error" || cmd == "clear_diag") {
clearRegisterDiag();
Serial.println("register diag cleared");
} else if (cmd == "help") {
Serial.println("commands: last_error, last_diag, reg_diag, clear_error, clear_diag, help");
} else {
Serial.println(String("unknown command: ") + cmd);
}
continue;
}
if (gSerialCommandBuffer.length() < 120) {
gSerialCommandBuffer += ch;
}
}
}
static String loginDisplayValue() {
return gLoginValue.isEmpty() ? "login not set" : gLoginValue;
}
@ -4128,9 +4343,11 @@ void setup() {
rebuildScreen();
Serial.println("Minimal nav test ready");
Serial.println("USB diag commands: last_error, last_diag, reg_diag, clear_error, help");
}
void loop() {
handleUsbSerialCommands();
lv_timer_handler();
manageWifiReconnect();
manageAccountPdaRefresh();

View File

@ -1,2 +1,2 @@
client.version=1.2.168
server.version=1.2.157
client.version=1.2.169
server.version=1.2.158