Переделать remote AddBlock на сборку блока на homeserver

This commit is contained in:
AidarKC 2026-06-28 14:45:29 +04:00
parent 3068c3e2b8
commit ed83b1f906
4 changed files with 319 additions and 69 deletions

View File

@ -300,11 +300,11 @@
То есть телефон без локального `blockchain.key` может:
- подготовить unsigned preimage блока;
- подготовить только сырой payload операции без текущей вершины цепочки;
- подписать сам `SendSignal` своим `session key`;
- дополнительно подписать его `client key`, чтобы homeserver/ESP32 точно видел, что запрос пришёл от доверенного клиента этого же логина;
- отправить запрос в выбранную `homeserver`-сессию;
- получить от неё ответ после настоящего `AddBlock`.
- получить от неё ответ после настоящего `AddBlock`, который homeserver соберёт и подпишет уже сама.
### Режимы доставки
@ -348,7 +348,7 @@
"targetSessionId": "sess-hs-001",
"signalType": "remote_addblock_request",
"signalRequestId": "remote-addblock-001",
"data": "{\"operation\":\"remote_addblock_request\",\"login\":\"alice\",\"blockchainName\":\"alice_main\",\"blockNumber\":152,\"prevBlockHash\":\"abc...\",\"blockPreimageB64\":\"...\"}",
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
"timeMs": 1774700000123,
"sessionSignatureB64": "BASE64_64",
"clientSignatureB64": "BASE64_64"
@ -385,7 +385,7 @@
"targetSessionId": "sess-hs-001",
"signalType": "remote_addblock_request",
"signalRequestId": "remote-addblock-001",
"data": "{\"operation\":\"remote_addblock_request\",\"login\":\"alice\",\"blockchainName\":\"alice_main\",\"blockNumber\":152,\"prevBlockHash\":\"abc...\",\"blockPreimageB64\":\"...\"}",
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
"timeMs": 1774700000123,
"sessionSignatureB64": "BASE64_64",
"clientSignatureB64": "BASE64_64",
@ -394,6 +394,30 @@
}
```
### Специфика `remote AddBlock`
Для `remote_addblock_request` поле `data` теперь содержит:
- `blockchainName`
- `blockBodyB64`
Где `blockBodyB64` — это не финальный блок и не почти готовый preimage, а компактный бинарный контейнер:
- `msgType` (`u16`)
- `msgSubType` (`u16`)
- `msgVersion` (`u16`)
- `bodyBytes`
После этого homeserver сама:
- вызывает `GetUser(login)` и получает `serverLastGlobalNumber/serverLastGlobalHash`;
- вычисляет новый `blockNumber = last + 1`;
- подставляет актуальный `prevBlockHash`;
- ставит текущее время;
- досчитывает полный preimage;
- подписывает его своим `blockchain key`;
- и только потом делает настоящий `AddBlock`.
### Специфические коды ошибок `SendSignal`
- `422 / NOT_AUTHENTICATED` — требуется авторизация.

View File

@ -10,7 +10,9 @@
- клиент без локального `blockchain.key` выбирает `homeserver`-сессию (`sessionType = 100`);
- клиент отправляет в неё `remote_addblock_request` через `SendSignal`;
- запрос подписывается `session key` и `client key`;
- ESP32/homeserver автоматически подписывает настоящий `AddBlock` своим `blockchain key` и сам отправляет его на сервер;
- UI больше не передаёт `blockNumber` и `prevBlockHash`;
- UI передаёт только `blockchainName + blockBodyB64`;
- ESP32/homeserver сама делает `GetUser(login)`, получает актуальную вершину цепочки, собирает финальный блок, подписывает настоящий `AddBlock` своим `blockchain key` и сам отправляет его на сервер;
- результат возвращается назад сигналом `remote_addblock_result`.
## Что проверить вручную

View File

@ -510,11 +510,13 @@ static void saveShineSessionPrefs();
static String normalizeLoginValue(const String &value);
static bool base58ToFixed32(const String &value, uint8_t out[32]);
static bool base64DecodeStd(const String &value, std::vector<uint8_t> &out);
static bool hex64ToBytes(const String &value, uint8_t out[32]);
static String bytesToBase64String(const uint8_t *data, size_t len);
static String jsonEscape(const String &value);
static bool jsonStringField(const String &json, const String &field, String &valueOut);
static bool jsonBoolField(const String &json, const String &field, bool &valueOut);
static bool jsonInt64Field(const String &json, const String &field, uint64_t &valueOut);
static bool jsonSignedInt64Field(const String &json, const String &field, int64_t &valueOut);
static bool parseUrlHostPortPath(const String &url, String &hostOut, uint16_t &portOut, String &pathOut, bool &secureOut);
static String formatPairingShortCode(const String &value);
static bool pairingMenuVisible();
@ -938,6 +940,30 @@ static String normalizeLoginValue(const String &value) {
return out;
}
static int hexNibbleValue(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
return -1;
}
static bool hex64ToBytes(const String &value, uint8_t out[32]) {
String clean = value;
clean.trim();
if (clean.length() != 64) {
return false;
}
for (size_t i = 0; i < 32; ++i) {
int hi = hexNibbleValue(clean.charAt((int)(i * 2)));
int lo = hexNibbleValue(clean.charAt((int)(i * 2 + 1)));
if (hi < 0 || lo < 0) {
return false;
}
out[i] = (uint8_t)((hi << 4) | lo);
}
return true;
}
static bool isValidShineServerLoginValue(const String &value) {
if (value.isEmpty() || value.length() > 20) {
return false;
@ -1625,6 +1651,34 @@ static bool jsonInt64Field(const String &json, const String &field, uint64_t &va
return true;
}
static bool jsonSignedInt64Field(const String &json, const String &field, int64_t &valueOut) {
String needle = "\"" + field + "\"";
int keyPos = json.indexOf(needle);
if (keyPos < 0) {
return false;
}
int colon = json.indexOf(':', keyPos + needle.length());
if (colon < 0) {
return false;
}
int pos = colon + 1;
while (pos < (int)json.length() && (json[pos] == ' ' || json[pos] == '\n' || json[pos] == '\r' || json[pos] == '\t')) {
pos++;
}
int start = pos;
if (pos < (int)json.length() && json[pos] == '-') {
pos++;
}
while (pos < (int)json.length() && isDigit((unsigned char)json[pos])) {
pos++;
}
if (pos == start || (pos == start + 1 && json[start] == '-')) {
return false;
}
valueOut = strtoll(json.substring(start, pos).c_str(), nullptr, 10);
return true;
}
static String formatSolValue(uint64_t lamports) {
uint64_t whole = lamports / 1000000000ULL;
uint64_t frac = (lamports % 1000000000ULL) / 1000000ULL;
@ -2853,6 +2907,75 @@ static bool sendSignalResponse(const String &toLogin,
return shineWsRequest(gShineWs, "SendSignal", req, response, SHINE_RPC_TIMEOUT_MS);
}
static void appendUint16BE(std::vector<uint8_t> &out, uint16_t value) {
out.push_back((uint8_t)((value >> 8) & 0xFF));
out.push_back((uint8_t)(value & 0xFF));
}
static void appendInt32BE(std::vector<uint8_t> &out, int32_t value) {
uint32_t v = (uint32_t)value;
out.push_back((uint8_t)((v >> 24) & 0xFF));
out.push_back((uint8_t)((v >> 16) & 0xFF));
out.push_back((uint8_t)((v >> 8) & 0xFF));
out.push_back((uint8_t)(v & 0xFF));
}
static void appendInt64BE(std::vector<uint8_t> &out, int64_t value) {
uint64_t v = (uint64_t)value;
out.push_back((uint8_t)((v >> 56) & 0xFF));
out.push_back((uint8_t)((v >> 48) & 0xFF));
out.push_back((uint8_t)((v >> 40) & 0xFF));
out.push_back((uint8_t)((v >> 32) & 0xFF));
out.push_back((uint8_t)((v >> 24) & 0xFF));
out.push_back((uint8_t)((v >> 16) & 0xFF));
out.push_back((uint8_t)((v >> 8) & 0xFF));
out.push_back((uint8_t)(v & 0xFF));
}
static uint16_t readUint16BE(const uint8_t *data) {
return (uint16_t)(((uint16_t)data[0] << 8) | (uint16_t)data[1]);
}
static bool fetchRemoteAddBlockCursor(const String &login,
String &blockchainNameOut,
int32_t &lastBlockNumberOut,
String &lastBlockHashOut,
String &errorMessageOut,
String &errorCodeOut) {
const char *kRemoteZeroHash64 = "0000000000000000000000000000000000000000000000000000000000000000";
blockchainNameOut = "";
lastBlockNumberOut = -1;
lastBlockHashOut = String(kRemoteZeroHash64);
errorMessageOut = "";
errorCodeOut = "";
String getUserReq = String("{\"login\":\"") + jsonEscape(login) + "\"}";
String getUserResp;
bool ok = shineWsRequest(gShineWs, "GetUser", getUserReq, getUserResp, SHINE_RPC_TIMEOUT_MS);
uint64_t statusCode = 0;
jsonInt64Field(getUserResp, "status", statusCode);
if (!ok || statusCode != 200) {
jsonStringField(getUserResp, "message", errorMessageOut);
jsonStringField(getUserResp, "code", errorCodeOut);
if (errorCodeOut.isEmpty()) errorCodeOut = ok ? "getuser_rejected" : "getuser_request_failed";
if (errorMessageOut.isEmpty()) errorMessageOut = ok ? "GetUser rejected by server" : "GetUser request failed";
return false;
}
int64_t lastBlockNumberI64 = -1;
jsonStringField(getUserResp, "blockchainName", blockchainNameOut);
jsonStringField(getUserResp, "serverLastGlobalHash", lastBlockHashOut);
if (!jsonSignedInt64Field(getUserResp, "serverLastGlobalNumber", lastBlockNumberI64)) {
lastBlockNumberI64 = -1;
}
lastBlockNumberOut = (int32_t)lastBlockNumberI64;
if (lastBlockHashOut.isEmpty()) {
lastBlockHashOut = String(kRemoteZeroHash64);
}
lastBlockHashOut.toLowerCase();
return true;
}
static void queueWalletSignRequest(const PendingWalletRpcRequest &item,
const String &requestId,
const String &publicKeyBase58,
@ -4393,11 +4516,8 @@ static void processPendingRemoteAddBlockRequests() {
gPendingRemoteAddBlockRequests.erase(gPendingRemoteAddBlockRequests.begin());
String responseData;
String requestLogin;
String blockchainName;
String prevBlockHash;
String blockPreimageB64;
uint64_t blockNumberU64 = 0;
String blockBodyB64;
if (item.fromLogin != gLoginValue) {
responseData = String("{\"ok\":false,\"error\":\"forbidden_login\",\"errorMessage\":\"Signal login mismatch\",\"requestId\":\"")
@ -4412,26 +4532,68 @@ static void processPendingRemoteAddBlockRequests() {
continue;
}
jsonStringField(item.data, "login", requestLogin);
jsonStringField(item.data, "blockchainName", blockchainName);
jsonStringField(item.data, "prevBlockHash", prevBlockHash);
jsonStringField(item.data, "blockPreimageB64", blockPreimageB64);
jsonInt64Field(item.data, "blockNumber", blockNumberU64);
if (requestLogin != gLoginValue || blockchainName.isEmpty() || blockPreimageB64.isEmpty() || prevBlockHash.isEmpty()) {
jsonStringField(item.data, "blockBodyB64", blockBodyB64);
if (blockchainName.isEmpty() || blockBodyB64.isEmpty()) {
responseData = String("{\"ok\":false,\"error\":\"bad_request\",\"errorMessage\":\"Missing required AddBlock fields\",\"requestId\":\"")
+ jsonEscape(item.signalRequestId) + "\"}";
sendSignalResponse(item.fromLogin, item.fromSessionId, "remote_addblock_result", item.signalRequestId, responseData);
continue;
}
std::vector<uint8_t> preimage;
if (!base64DecodeStd(blockPreimageB64, preimage) || preimage.empty()) {
responseData = String("{\"ok\":false,\"error\":\"bad_preimage_base64\",\"errorMessage\":\"Invalid AddBlock preimage base64\",\"requestId\":\"")
std::vector<uint8_t> remoteBody;
if (!base64DecodeStd(blockBodyB64, remoteBody) || remoteBody.size() < 6) {
responseData = String("{\"ok\":false,\"error\":\"bad_block_body_base64\",\"errorMessage\":\"Invalid remote block body base64\",\"requestId\":\"")
+ jsonEscape(item.signalRequestId) + "\"}";
sendSignalResponse(item.fromLogin, item.fromSessionId, "remote_addblock_result", item.signalRequestId, responseData);
continue;
}
uint16_t msgType = readUint16BE(remoteBody.data());
uint16_t msgSubType = readUint16BE(remoteBody.data() + 2);
uint16_t msgVersion = readUint16BE(remoteBody.data() + 4);
std::vector<uint8_t> bodyBytes(remoteBody.begin() + 6, remoteBody.end());
String resolvedBlockchainName;
int32_t lastBlockNumber = -1;
String prevBlockHash;
String cursorError;
String cursorErrorCode;
if (!fetchRemoteAddBlockCursor(gLoginValue, resolvedBlockchainName, lastBlockNumber, prevBlockHash, cursorError, cursorErrorCode)) {
responseData = String("{\"ok\":false,\"error\":\"") + jsonEscape(cursorErrorCode)
+ "\",\"errorMessage\":\"" + jsonEscape(cursorError)
+ "\",\"requestId\":\"" + jsonEscape(item.signalRequestId) + "\"}";
sendSignalResponse(item.fromLogin, item.fromSessionId, "remote_addblock_result", item.signalRequestId, responseData);
continue;
}
if (resolvedBlockchainName.isEmpty()) {
resolvedBlockchainName = blockchainName;
}
int32_t nextBlockNumber = lastBlockNumber + 1;
String cleanPrevHash = prevBlockHash.isEmpty()
? String("0000000000000000000000000000000000000000000000000000000000000000")
: prevBlockHash;
uint8_t prevHash32[32] = {};
if (!hex64ToBytes(cleanPrevHash, prevHash32)) {
responseData = String("{\"ok\":false,\"error\":\"bad_prev_hash\",\"errorMessage\":\"Invalid previous block hash from GetUser\",\"requestId\":\"")
+ jsonEscape(item.signalRequestId) + "\"}";
sendSignalResponse(item.fromLogin, item.fromSessionId, "remote_addblock_result", item.signalRequestId, responseData);
continue;
}
std::vector<uint8_t> preimage;
preimage.reserve(2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + bodyBytes.size());
appendUint16BE(preimage, 0);
preimage.insert(preimage.end(), prevHash32, prevHash32 + 32);
int32_t blockSize = (int32_t)(2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + bodyBytes.size());
appendInt32BE(preimage, blockSize);
appendInt32BE(preimage, nextBlockNumber);
appendInt64BE(preimage, (int64_t)(shineNowMs() / 1000ULL));
appendUint16BE(preimage, msgType);
appendUint16BE(preimage, msgSubType);
appendUint16BE(preimage, msgVersion);
preimage.insert(preimage.end(), bodyBytes.begin(), bodyBytes.end());
uint8_t blockchainSeed[32] = {};
uint8_t blockchainPub[32] = {};
uint8_t blockchainSec[64] = {};
@ -4456,9 +4618,9 @@ static void processPendingRemoteAddBlockRequests() {
fullBlock.push_back(0x01);
fullBlock.push_back(0x00);
fullBlock.insert(fullBlock.end(), signature, signature + 64);
String addBlockReq = String("{\"blockchainName\":\"") + jsonEscape(blockchainName)
+ "\",\"blockNumber\":" + String((unsigned long long)blockNumberU64)
+ ",\"prevBlockHash\":\"" + jsonEscape(prevBlockHash)
String addBlockReq = String("{\"blockchainName\":\"") + jsonEscape(resolvedBlockchainName)
+ "\",\"blockNumber\":" + String((long long)nextBlockNumber)
+ ",\"prevBlockHash\":\"" + jsonEscape(cleanPrevHash)
+ "\",\"blockBytesB64\":\"" + jsonEscape(bytesToBase64String(fullBlock.data(), fullBlock.size())) + "\"}";
String addBlockResp;
bool addBlockOk = shineWsRequest(gShineWs, "AddBlock", addBlockReq, addBlockResp, SHINE_RPC_TIMEOUT_MS);

View File

@ -751,6 +751,16 @@ function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType
);
}
function buildRemoteBlockBodyBytes({ msgType, msgSubType, msgVersion = 1, bodyBytes }) {
const body = bodyBytes || new Uint8Array(0);
return concatBytes(
int16Bytes(msgType),
int16Bytes(msgSubType),
int16Bytes(msgVersion),
body,
);
}
export class AuthService {
constructor(serverUrl) {
this.serverUrl = normalizeServerUrl(serverUrl);
@ -1437,6 +1447,65 @@ export class AuthService {
};
}
async submitRemoteAddBlockBody({ login, storagePwd, blockchainName, blockBodyBytes }) {
const cleanLogin = String(login || '').trim();
const cleanBlockchainName = String(blockchainName || '').trim();
if (!cleanLogin || !cleanBlockchainName) throw new Error('submitRemoteAddBlockBody: missing login/blockchainName');
if (!(blockBodyBytes instanceof Uint8Array) || blockBodyBytes.length < 6) {
throw new Error('submitRemoteAddBlockBody: bad blockBodyBytes');
}
const remoteSessionId = String(this.remoteAddBlockSessionId || '').trim();
if (!remoteSessionId) {
throw new Error('На устройстве нет blockchain key и не выбрана homeserver-сессия для remote AddBlock');
}
const signalRequestId = createSignalRequestId('remote-addblock');
const responseWait = this.waitForSignal({
signalType: SIGNAL_TYPE_REMOTE_ADDBLOCK_RESULT,
signalRequestId,
timeoutMs: 20000,
});
const signalData = {
operation: SIGNAL_TYPE_REMOTE_ADDBLOCK_REQUEST,
signalRequestId,
blockchainName: cleanBlockchainName,
blockBodyB64: bytesToBase64(blockBodyBytes),
};
await this.sendSignal({
toLogin: cleanLogin,
targetMode: SIGNAL_TARGET_SINGLE,
targetSessionId: remoteSessionId,
signalType: SIGNAL_TYPE_REMOTE_ADDBLOCK_REQUEST,
signalRequestId,
data: JSON.stringify(signalData),
storagePwd,
includeClientSignature: true,
});
const signalPayload = await responseWait;
let result = {};
try {
result = JSON.parse(String(signalPayload?.data || '{}'));
} catch {
throw new Error('Некорректный ответ remote AddBlock от homeserver');
}
if (!result?.ok) {
throw new Error(String(result?.errorMessage || result?.error || 'remote_addblock_failed'));
}
return {
status: 200,
payload: {
serverLastGlobalNumber: Number(result?.serverLastGlobalNumber ?? -1),
serverLastGlobalHash: String(result?.serverLastGlobalHash || ZERO_HASH_HEX),
remote: true,
},
};
}
async runAddBlockWithRetry({ login, storagePwd, resolveFreshState, buildPreimage }) {
let freshState = await resolveFreshState();
let blockchainName = String(freshState?.blockchainName || '').trim();
@ -1484,6 +1553,21 @@ export class AuthService {
if (!cleanLogin) throw new Error('Missing login for AddBlock');
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
const keyBundle = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
const blockchainPrivatePkcs8 = String(keyBundle?.blockchainKey || '').trim();
if (!blockchainPrivatePkcs8) {
const user = await this.getUser(cleanLogin);
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
if (!blockchainName) throw new Error('Не удалось определить blockchainName для remote AddBlock');
const response = await this.submitRemoteAddBlockBody({
login: cleanLogin,
storagePwd,
blockchainName,
blockBodyBytes: buildRemoteBlockBodyBytes({ msgType, msgSubType, msgVersion, bodyBytes }),
});
return response.payload || {};
}
const { response, blockchainName } = await this.runAddBlockWithRetry({
login: cleanLogin,
storagePwd,
@ -2458,11 +2542,6 @@ export class AuthService {
if (!cleanLogin || !cleanParam) throw new Error('Не переданы login/param.');
if (!cleanValue) throw new Error('Значение параметра не может быть пустым.');
if (!storagePwd) throw new Error('Не передан storagePwd для подписи AddBlock.');
const { response } = await this.runAddBlockWithRetry({
login: cleanLogin,
storagePwd,
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
buildPreimage: async ({ blockNumber, prevBlockHash }) => {
const bodyBytes = makeUserParamBodyBytes({
lineCode: 0,
prevLineNumber: -1,
@ -2471,20 +2550,14 @@ export class AuthService {
key: cleanParam,
value: cleanValue,
});
return concatBytes(
int16Bytes(0),
hexToBytes(prevBlockHash),
int32Bytes(2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + bodyBytes.length),
int32Bytes(blockNumber),
int64Bytes(Math.floor(Date.now() / 1000)),
int16Bytes(4),
int16Bytes(1),
int16Bytes(1),
return this.addBlockSigned({
login: cleanLogin,
storagePwd,
msgType: 4,
msgSubType: 1,
msgVersion: 1,
bodyBytes,
);
},
});
return response.payload || {};
}
async addBlockConnection({ login, toLogin, subType, storagePwd }) {
@ -2504,11 +2577,6 @@ export class AuthService {
const targetUser = await this.getUser(cleanToLogin);
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
const toBlockchainName = String(targetUser?.blockchainName || `${cleanToLogin}-${BCH_SUFFIX}`).trim();
const { response } = await this.runAddBlockWithRetry({
login: cleanLogin,
storagePwd,
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
buildPreimage: async ({ blockNumber, prevBlockHash }) => {
const bodyBytes = makeConnectionBodyBytes({
lineCode: 0,
prevLineNumber: -1,
@ -2518,20 +2586,14 @@ export class AuthService {
toBlockNumber: 0,
toBlockHashHex: ZERO_HASH_HEX,
});
return concatBytes(
int16Bytes(0),
hexToBytes(prevBlockHash),
int32Bytes(2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + bodyBytes.length),
int32Bytes(blockNumber),
int64Bytes(Math.floor(Date.now() / 1000)),
int16Bytes(3),
int16Bytes(cleanSubType),
int16Bytes(1),
return this.addBlockSigned({
login: cleanLogin,
storagePwd,
msgType: 3,
msgSubType: cleanSubType,
msgVersion: 1,
bodyBytes,
);
},
});
return response.payload || {};
}