back32hash 5591: готово к тестам

Вроде как всё готово к тестам.
This commit is contained in:
AidarKC
2026-10-02 01:32:37 +03:00
parent e101e5c5f4
commit de8700f8e0
51 changed files with 337 additions and 18091 deletions
+31 -27
View File
@@ -39,6 +39,7 @@ import {
} from './key-vault.js';
import { createAns104DataItem, parseAns104DataItem } from './ans104-data-item.js';
import { defaultServerWs } from '../deploy-config.js';
import { SHINE_APP_TAG_NAME, SHINE_APP_TAG_VALUE, SHINE_CHANNEL_TAG_NAME, SHINE_TYPE_TAG_NAME } from './protocol-tags.js';
const BCH_SUFFIX = '001';
const ZERO64 = '0'.repeat(64);
@@ -896,7 +897,7 @@ function bytesToHexLower(bytes) {
function parseStoredLineMeta(blockBytesB64, { fixedLine = false } = {}) {
const raw = base64ToBytes(String(blockBytesB64 || ''));
const frame = parseAns104DataItem(raw).data;
const bodyOffset = 56;
const bodyOffset = 88;
const minLength = fixedLine ? bodyOffset + 40 : bodyOffset + 44;
if (!(frame instanceof Uint8Array) || frame.length < minLength) throw new Error('Некорректный line-блок');
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
@@ -934,14 +935,16 @@ function isLineConflictError(error) {
]).has(String(error?.code || '').toUpperCase());
}
function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
function buildBlockPreimage({ prevBlockHashHex, back32HashHex, blockNumber, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
const prevHashBytes = hexToBytes(normalizeHex32(prevBlockHashHex));
const back32HashBytes = hexToBytes(normalizeHex32(back32HashHex, ZERO64));
const body = bodyBytes || new Uint8Array(0);
const blockSize = 2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + body.length;
const blockSize = 2 + 32 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + body.length;
return concatBytes(
int16Bytes(1),
prevHashBytes,
back32HashBytes,
int32Bytes(blockSize),
int32Bytes(blockNumber),
int64Bytes(Math.floor(Date.now() / 1000)),
@@ -1621,7 +1624,7 @@ export class AuthService {
owner32,
privateKey,
data: preimage,
tags: Array.isArray(ansTags) && ansTags.length ? ansTags : [{ name: 'App', value: 'test5590' }],
tags: Array.isArray(ansTags) && ansTags.length ? ansTags : [{ name: SHINE_APP_TAG_NAME, value: SHINE_APP_TAG_VALUE }],
});
return this.ws.request('AddBlock', {
blockchainName: cleanBlockchainName,
@@ -1753,7 +1756,13 @@ export class AuthService {
const tryAdd = async (cursor) => {
const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1;
const prevBlockHash = normalizeHex32(cursor?.serverLastGlobalHash, ZERO64);
const preimage = await buildPreimage({ blockNumber, prevBlockHash, blockchainName });
const back32Hash = blockNumber < 32
? ZERO64
: normalizeHex32(await this.getBlockHashByNumber(blockchainName, blockNumber - 32), ZERO64);
if (blockNumber >= 32 && back32Hash === ZERO64) {
throw new Error(`Не удалось получить hash блока #${blockNumber - 32} для back32Hash`);
}
const preimage = await buildPreimage({ blockNumber, prevBlockHash, back32Hash, blockchainName });
return this.submitPreparedAddBlock({
login,
storagePwd,
@@ -1813,19 +1822,20 @@ export class AuthService {
return response.payload || {};
}
const ansTags = [{ name: 'App', value: 'test5590' }];
const ansTags = [{ name: SHINE_APP_TAG_NAME, value: SHINE_APP_TAG_VALUE }];
const typeIndexTag = typeIndexTagValue(msgType);
if (typeIndexTag) ansTags.push({ name: 't', value: typeIndexTag });
if (typeIndexTag) ansTags.push({ name: SHINE_TYPE_TAG_NAME, value: typeIndexTag });
const cleanChannelSlug = String(channelSlug || '').trim();
if (cleanChannelSlug) ansTags.push({ name: 'c_test5590', value: cleanChannelSlug });
if (cleanChannelSlug) ansTags.push({ name: SHINE_CHANNEL_TAG_NAME, value: cleanChannelSlug });
const { response, blockchainName } = await this.runAddBlockWithRetry({
login: cleanLogin,
storagePwd,
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
ansTags,
buildPreimage: async ({ blockNumber, prevBlockHash }) => buildBlockPreimage({
buildPreimage: async ({ blockNumber, prevBlockHash, back32Hash }) => buildBlockPreimage({
prevBlockHashHex: prevBlockHash,
back32HashHex: back32Hash,
blockNumber,
msgType,
msgSubType,
@@ -2199,26 +2209,20 @@ export class AuthService {
}
async getBlockHashByNumber(blockchainName, blockNumber) {
const cleanBlockchainName = String(blockchainName || '').trim();
const cleanBlockNumber = Number(blockNumber);
try {
const payload = await this.getMessageThread(
{
blockchainName: String(blockchainName || '').trim(),
blockNumber: cleanBlockNumber,
blockHash: ZERO64,
},
0,
0,
1
);
const hash = payload?.focus?.messageRef?.blockHash;
return normalizeHex32(hash, ZERO64);
} catch (error) {
if (cleanBlockNumber === 0 && Number(error?.status) === 404) {
return ZERO64;
}
throw error;
if (!cleanBlockchainName || !Number.isInteger(cleanBlockNumber) || cleanBlockNumber < 0) {
throw new Error('Некорректный blockchainName/blockNumber для GetBlockchainBlock');
}
const response = await this.ws.request('GetBlockchainBlock', {
blockchainName: cleanBlockchainName,
blockNumber: cleanBlockNumber,
});
if (response.status === 200) {
return normalizeHex32(response?.payload?.blockHash, ZERO64);
}
if (cleanBlockNumber === 0 && Number(response?.status) === 404) return ZERO64;
throw opError('GetBlockchainBlock', response);
}
async resolveHeaderHashForBlockchain(blockchainName) {
+17 -14
View File
@@ -2,6 +2,7 @@ import { createAns104DataItem, createAns104DataItemWithRawParts, parseAns104Data
import { base64ToBytes, bytesToBase64 } from './crypto-utils.js';
import { updateShineUserPdaOnSolana } from './shine-user-pda-service.js';
import { updateEncryptedUserSecrets } from './key-vault.js';
import { SHINE_APP_TAG_NAME, SHINE_APP_TAG_VALUE, SHINE_TYPE_TAG_NAME } from './protocol-tags.js';
const TE = new TextEncoder();
const ZERO_HASH = '0'.repeat(64);
@@ -33,23 +34,23 @@ function hexToBytes(hex) {
function bytesToHex(bytes) { return Array.from(bytes || [], (b)=>b.toString(16).padStart(2,'0')).join(''); }
function normalizeComment(value) { return String(value || '').trim().replace(/\r\n/g,'\n').replace(/\r/g,'\n'); }
function readFrameTimestampMs(frame) {
if (!(frame instanceof Uint8Array) || frame.length < 50) throw new Error('Некорректный SHiNE Frame');
const seconds = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getBigInt64(42, false);
if (!(frame instanceof Uint8Array) || frame.length < 82) throw new Error('Некорректный SHiNE Frame');
const seconds = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getBigInt64(74, false);
return Number(seconds * 1000n);
}
function readFrameType(frame) {
if (!(frame instanceof Uint8Array) || frame.length < 56) throw new Error('Некорректный SHiNE Frame');
if (!(frame instanceof Uint8Array) || frame.length < 88) throw new Error('Некорректный SHiNE Frame');
const view=new DataView(frame.buffer,frame.byteOffset,frame.byteLength);
return { type:view.getUint16(50,false), subType:view.getUint16(52,false) };
return { type:view.getUint16(82,false), subType:view.getUint16(84,false) };
}
function readExplicitLinePrefix(frame) {
if (!(frame instanceof Uint8Array) || frame.length < 100) throw new Error('Некорректный line Frame');
if (!(frame instanceof Uint8Array) || frame.length < 132) throw new Error('Некорректный line Frame');
const view=new DataView(frame.buffer,frame.byteOffset,frame.byteLength);
return {
lineCode:view.getInt32(56,false),
prevLineNumber:view.getInt32(60,false),
prevLineHash:bytesToHex(frame.slice(64,96)),
lineSequence:view.getInt32(96,false),
lineCode:view.getInt32(88,false),
prevLineNumber:view.getInt32(92,false),
prevLineHash:bytesToHex(frame.slice(96,128)),
lineSequence:view.getInt32(128,false),
};
}
function makeForkBody({ lineCode=0, prevLineNumber, prevLineHash, lineSequence, oldBlockchainKeyB64, forkPointBlock, forkPointHash, forkPointTimestampMs, parentTipBlock, parentTipHash, parentTipTimestampMs, reasonCode, comment }) {
@@ -62,10 +63,10 @@ function makeForkBody({ lineCode=0, prevLineNumber, prevLineHash, lineSequence,
}
return concat(be32(lineCode),be32(prevLineNumber),hexToBytes(prevLineHash),be32(lineSequence),oldKey,be32(forkPointBlock),hexToBytes(forkPointHash),be64(forkPointTimestampMs),be32(parentTipBlock),hexToBytes(parentTipHash),be64(parentTipTimestampMs),be32(discarded),Uint8Array.of(Number(reasonCode)),be16(c.length),c);
}
function makeFrame({ prevHash, blockNumber, type, subType, version=1, body, timestampMs=Date.now() }) {
function makeFrame({ prevHash, back32Hash=ZERO_HASH, blockNumber, type, subType, version=1, body, timestampMs=Date.now() }) {
const bodyBytes=body || new Uint8Array(0);
const size=2+32+4+4+8+2+2+2+bodyBytes.length;
return concat(be16(1),hexToBytes(prevHash),be32(size),be32(blockNumber),be64(Math.floor(Number(timestampMs)/1000)),be16(type),be16(subType),be16(version),bodyBytes);
const size=2+32+32+4+4+8+2+2+2+bodyBytes.length;
return concat(be16(1),hexToBytes(prevHash),hexToBytes(back32Hash),be32(size),be32(blockNumber),be64(Math.floor(Number(timestampMs)/1000)),be16(type),be16(subType),be16(version),bodyBytes);
}
export const KEY_ROTATION_REASONS = Object.freeze([
@@ -153,8 +154,10 @@ export class KeyRotationClient {
comment:rotation.comment,
});
const techTimestampMs=Number(rotation.createdAtMs)||readFrameTimestampMs(tipParsed.data)+1000;
const techFrame=makeFrame({ prevHash:rotation.forkFromHash, blockNumber:cutoff+1, type:0, subType:TECH_FORK_SUBTYPE, version:1, body:forkBody, timestampMs:techTimestampMs });
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:'App',value:'test5590'},{name:'t',value:'tech'}] });
const techBlockNumber=cutoff+1;
const techBack32Hash=techBlockNumber<32 ? ZERO_HASH : String((await this.getBlock(source,techBlockNumber-32)).blockHash||'');
const techFrame=makeFrame({ prevHash:rotation.forkFromHash, back32Hash:techBack32Hash, blockNumber:techBlockNumber, type:0, subType:TECH_FORK_SUBTYPE, version:1, body:forkBody, timestampMs:techTimestampMs });
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:SHINE_APP_TAG_NAME,value:SHINE_APP_TAG_VALUE},{name:SHINE_TYPE_TAG_NAME,value:'tech'}] });
await this.addCandidate({ blockNumber:cutoff+1, prevBlockHash:rotation.forkFromHash, blockBytesB64:bytesToBase64(techRaw) });
onProgress({ phase:'copy', current:total, total });
}
+7
View File
@@ -0,0 +1,7 @@
// Transport namespace UI. При следующей смене тестовой эпохи менять только эту строку в UI.
export const SHINE_PROTOCOL_NAMESPACE = 'test5591';
export const SHINE_APP_TAG_NAME = 'App';
export const SHINE_APP_TAG_VALUE = SHINE_PROTOCOL_NAMESPACE;
export const SHINE_CHANNEL_TAG_NAME = 'c';
export const SHINE_TYPE_TAG_NAME = 't';
@@ -25,16 +25,16 @@ index.html?key=<BASE58_BLOCKCHAIN_PUBLIC_KEY>&channel=books
&solanaRpc=https://api.mainnet-beta.solana.com
```
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5590` и строит список каналов из найденных блоков.
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5591` и строит список каналов из найденных блоков.
## Что делает viewer
1. Если передан `login`, вычисляет PDA `user_login=<login>` программы `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6` и читает `blockchainKey`.
2. Если передан `key`, использует его напрямую и не обращается к Solana.
3. Из Ed25519/Solana public key вычисляет Arweave owner address как `base64url(SHA-256(pubkey32))`.
4. Делает Arweave GraphQL запрос по owner + `App=test5590`; `channel` применяется локально после загрузки авторского блокчейна.
4. Делает Arweave GraphQL запрос по owner + `App=test5591`; `channel` применяется локально после загрузки авторского блокчейна.
5. Загружает payload каждого DataItem через gateway `/<DataItemId>`.
6. Разбирает payload как SHiNE Frame v1 (`frameCode=0x0001`, header 56 bytes).
6. Разбирает payload как SHiNE Frame v1 (`frameCode=0x0001`, header 88 bytes).
7. Дедуплицирует повторно опубликованные одинаковые пользовательские блоки по SHA-256 Frame v1.
8. Восстанавливает канал/сообщения тем же body parser, который использовал старый viewer.
+28 -13
View File
@@ -1097,9 +1097,12 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
const BLOCK_TYPE_SESSIONS = 50;
const BLOCK_TYPE_TRUSTED_STATE = 70;
const BLOCK_TYPE_ARCHIVE_HEAD = 80;
const BCH_FRAME_HEADER_SIZE = 56;
const BCH_FRAME_HEADER_SIZE = 88;
const BCH_FRAME_CODE_V1 = 0x0001;
const SHINE_ARWEAVE_APP_TAG = 'test5590';
// Transport namespace актуального standalone Viewer.
const SHINE_PROTOCOL_NAMESPACE = 'test5591';
const SHINE_ARWEAVE_APP_TAG = SHINE_PROTOCOL_NAMESPACE;
const SHINE_ARWEAVE_CHANNEL_TAG = 'c';
const BCH_TYPE_TECH = 0;
const BCH_TYPE_TEXT = 1;
const BCH_SUBTYPE_HEADER = 0;
@@ -2058,22 +2061,23 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
if (frameCode !== BCH_FRAME_CODE_V1) {
throw new Error(`Неожиданный frameCode 0x${frameCode.toString(16)} (ожидался 0x0001)`);
}
const blockSize = readInt32BE(bytes, 34);
const blockSize = readInt32BE(bytes, 66);
if (blockSize !== bytes.length || blockSize < BCH_FRAME_HEADER_SIZE) {
throw new Error(`Frame size mismatch: blockSize=${blockSize}, actual=${bytes.length}`);
}
const hashBytes = await digestSha256(bytes);
const type = readUint16BE(bytes, 50);
const subType = readUint16BE(bytes, 52);
const version = readUint16BE(bytes, 54);
const type = readUint16BE(bytes, 82);
const subType = readUint16BE(bytes, 84);
const version = readUint16BE(bytes, 86);
const bodyBytes = bytes.slice(BCH_FRAME_HEADER_SIZE);
const body = parseBlockchainBody(type, subType, version, bodyBytes);
return {
frameCode,
prevHashHex: bytesToHex(bytes.slice(2, 34)),
back32HashHex: bytesToHex(bytes.slice(34, 66)),
blockSize,
blockNumber: readInt32BE(bytes, 38),
timestampSec: readInt64BE(bytes, 42),
blockNumber: readInt32BE(bytes, 70),
timestampSec: readInt64BE(bytes, 74),
type,
subType,
version,
@@ -2501,7 +2505,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
let offset = 0;
while (offset < data.length) {
if (data.length - offset < BCH_FRAME_HEADER_SIZE) throw new Error(`Frame обрывается на смещении ${offset}`);
const blockSize = readInt32BE(data, offset + 34);
const blockSize = readInt32BE(data, offset + 66);
if (blockSize < BCH_FRAME_HEADER_SIZE || offset + blockSize > data.length) throw new Error(`Некорректный blockSize ${blockSize} на смещении ${offset}`);
blocks.push(await parseBlockchainBlock(data.slice(offset, offset + blockSize)));
offset += blockSize;
@@ -3345,7 +3349,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
const oldSetContentStatus = setContentStatus;
setContentStatus = function(kind,text){ oldSetContentStatus(kind,text); const el=document.getElementById('v3-status'); if(el) el.textContent=text; v3Log(text); };
function v3Theme(theme){ document.documentElement.dataset.theme=theme; localStorage.setItem('shine.viewer.theme',theme); const b=document.getElementById('v3-theme'); if(b)b.textContent=theme==='dark'?'☀':'☾'; }
function v3RenderBlocks(){ const host=document.getElementById('v3-block-view'); if(!host)return; host.innerHTML=''; const wrap=document.createElement('div'); wrap.className='v3-blocks'; const blocks=(currentBlockchainView?.rawBlocks||V3.blocks||[]).slice().sort((a,b)=>b.blockNumber-a.blockNumber); blocks.forEach(b=>{ const d=document.createElement('details'); d.className='v3-block'; const sm=document.createElement('summary'); sm.textContent=`#${b.blockNumber} · type ${b.type}/${b.subType} · ${formatTimestamp(Number(b.timestampSec)*1000)}`; const pre=document.createElement('pre'); pre.textContent=JSON.stringify({blockNumber:b.blockNumber,type:b.type,subType:b.subType,version:b.version,hash:b.hashHex,prevHash:b.prevHashHex,arweaveId:b.arweaveId,body:b.body},(k,v)=>typeof v==='bigint'?String(v):v,2); d.append(sm,pre); wrap.appendChild(d); }); if(!blocks.length)wrap.textContent='Блоки ещё не загружены.'; host.appendChild(wrap); }
function v3RenderBlocks(){ const host=document.getElementById('v3-block-view'); if(!host)return; host.innerHTML=''; const wrap=document.createElement('div'); wrap.className='v3-blocks'; const blocks=(currentBlockchainView?.rawBlocks||V3.blocks||[]).slice().sort((a,b)=>b.blockNumber-a.blockNumber); blocks.forEach(b=>{ const d=document.createElement('details'); d.className='v3-block'; const sm=document.createElement('summary'); sm.textContent=`#${b.blockNumber} · type ${b.type}/${b.subType} · ${formatTimestamp(Number(b.timestampSec)*1000)}`; const pre=document.createElement('pre'); pre.textContent=JSON.stringify({blockNumber:b.blockNumber,type:b.type,subType:b.subType,version:b.version,hash:b.hashHex,prevHash:b.prevHashHex,back32Hash:b.back32HashHex,arweaveId:b.arweaveId,body:b.body},(k,v)=>typeof v==='bigint'?String(v):v,2); d.append(sm,pre); wrap.appendChild(d); }); if(!blocks.length)wrap.textContent='Блоки ещё не загружены.'; host.appendChild(wrap); }
function v3SetView(mode){ const bv=document.getElementById('v3-block-view'), cb=document.getElementById('content-browser'); document.getElementById('v3-channel-tab').classList.toggle('is-active',mode==='channel'); document.getElementById('v3-blocks-tab').classList.toggle('is-active',mode==='blocks'); if(bv)bv.hidden=mode!=='blocks'; if(cb)cb.hidden=mode==='blocks'; if(mode==='blocks')v3RenderBlocks(); }
async function v3ClearCache(){ try{ const keys=Object.keys(localStorage).filter(k=>k.startsWith('shine.viewer.data.')); keys.forEach(k=>localStorage.removeItem(k)); if('caches' in window){ for(const k of await caches.keys()) if(k.startsWith('shine-viewer-')) await caches.delete(k); } v3Log('Кэш данных очищен'); }catch(e){v3Log('Ошибка очистки кэша: '+e.message);} }
function v3CacheKey(key,channel){return 'shine.viewer.author-chain.'+String(key);}
@@ -3372,7 +3376,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
function v3Verification(blocks){
blocks=(blocks||[]).slice().sort((a,b)=>a.blockNumber-b.blockNumber);
const byNum=new Map(blocks.map(b=>[Number(b.blockNumber),b]));
const result={total:blocks.length,frameHash:{ok:blocks.length,bad:0,na:0},global:{ok:0,bad:0,na:0},channel:{ok:0,bad:0,na:0},owner:{ok:0,bad:0,na:0},signature:{ok:0,bad:0,na:blocks.length},issues:[]};
const result={total:blocks.length,frameHash:{ok:blocks.length,bad:0,na:0},global:{ok:0,bad:0,na:0},back32:{ok:0,bad:0,na:0},channel:{ok:0,bad:0,na:0},owner:{ok:0,bad:0,na:0},signature:{ok:0,bad:0,na:blocks.length},issues:[]};
for(const b of blocks){
// hashHex is SHA-256(Frame) calculated locally by parseBlockchainBlock.
if(!b.hashHex){result.frameHash.ok--;result.frameHash.na++;}
@@ -3384,6 +3388,16 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
else if(String(prev.hashHex).toLowerCase()===String(b.prevHashHex).toLowerCase()) result.global.ok++;
else{result.global.bad++;result.issues.push({kind:'global',state:'bad',block:b.blockNumber,text:`prevHash не совпадает с SHA-256 блока #${prev.blockNumber}`});}
}
const back32=String(b.back32HashHex||'');
if(Number(b.blockNumber)<32){
if(v3ZeroHash(back32)) result.back32.ok++;
else{result.back32.bad++;result.issues.push({kind:'back32',state:'bad',block:b.blockNumber,text:'Для блока <32 back32Hash должен быть нулевым'});}
} else {
const target=byNum.get(Number(b.blockNumber)-32);
if(!target){result.back32.na++;result.issues.push({kind:'back32',state:'na',block:b.blockNumber,text:`Нет блока #${Number(b.blockNumber)-32} для проверки back32Hash`});}
else if(String(target.hashHex).toLowerCase()===back32.toLowerCase()) result.back32.ok++;
else{result.back32.bad++;result.issues.push({kind:'back32',state:'bad',block:b.blockNumber,text:`back32Hash не совпадает с SHA-256 блока #${Number(b.blockNumber)-32}`});}
}
const body=b.body||{};
if(body.kind==='text_line' || body.kind==='create_channel'){
const pn=Number(body.prevLineNumber||0), ph=String(body.prevLineHashHex||'');
@@ -3402,8 +3416,8 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
function v3UpdateVerification(){
const r=v3Verification(currentBlockchainView?.rawBlocks||V3.blocks||[]);
V3.verification=r;
const bad=r.global.bad+r.channel.bad+r.frameHash.bad+r.owner.bad+r.signature.bad;
const checked=r.global.ok+r.channel.ok;
const bad=r.global.bad+r.back32.bad+r.channel.bad+r.frameHash.bad+r.owner.bad+r.signature.bad;
const checked=r.global.ok+r.back32.ok+r.channel.ok;
const el=document.getElementById('v3-status');
if(el && r.total) el.textContent=`${bad?'⚠':'✓'} Хэши ${checked} · global ${v3MetricText(r.global)} · канал ${v3MetricText(r.channel)} · owner ${v3MetricText(r.owner)}`;
return r;
@@ -3416,6 +3430,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
document.getElementById('v3-verify-summary').innerHTML=
v3VerifyCard('SHA-256 Frame',r.frameHash,'вычислен локально')+
v3VerifyCard('Общая цепочка',r.global,'prevHash')+
v3VerifyCard('Резерв N−32',r.back32,'back32Hash')+
v3VerifyCard('Цепочки каналов',r.channel,'prevLineHash')+
v3VerifyCard('Arweave owner',r.owner,'совпадение выборки owner');
const sig=document.getElementById('v3-signature-note');