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
@@ -1,30 +0,0 @@
-proc:none
-d
/tmp/javac-check
-sourcepath
SHiNE-server/shine-server-geo/src/main/java:SHiNE-server/shine-server-log/src/main/java:SHiNE-server/shine-server-solana-users-sync/src/main/java:SHiNE-server/shine-server-db/src/main/java:SHiNE-server/src/main/java:SHiNE-server/shine-server-archive/src/main/java:SHiNE-server/shine-server-blockchain/src/main/java:SHiNE-server/shine-server-crypto/src/main/java:SHiNE-server/shine-server-net-protocol/src/main/java:SHiNE-server/shine-server-config/src/main/java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-net-protocol/src/main/java/server/sync/BlockchainResyncGuard.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler_utils/BlockchainWriter.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/Ans104DataItem.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BchCryptoVerifier.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BchBlockEntry.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveBlocksConfig.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveBlockSyncScheduler.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveMerkle.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveBlockSyncService.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveBlockPublisherScheduler.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/Ans104Bundle.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveL1Uploader.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-archive/src/main/java/server/archive/ArweaveBlockPublisherService.java
/mnt/data/shine_patch_final_work/SHiNE-server/src/main/java/server/ws/WsServer.java
/mnt/data/shine_patch_final_work/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java
/mnt/data/shine_patch_final_work/SHiNE-server/src/test/java/test/it/blockchain/AddBlockSender.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/BlockEntry.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ArweaveBlockImportDAO.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlockchainResyncCleanupDAO.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/BlocksDAO.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ChannelNameStateDAO.java
/mnt/data/shine_patch_final_work/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUserPdaCurrentDAO.java
@@ -26,7 +26,7 @@ import java.time.Duration;
import java.util.*;
/**
* Discovers individual App=test5590 DataItems and imports the exact signed ANS-104 bytes.
* Discovers individual App=test5591 DataItems and imports the exact signed ANS-104 bytes.
* The importer is transport-agnostic: a DataItem may have reached Arweave through Turbo
* or inside a direct server-created ANS-104 bundle.
*/
@@ -1,6 +1,7 @@
package server.archive;
import blockchain.Ans104DataItem;
import blockchain.ShineProtocolTags;
import utils.config.AppConfig;
import java.nio.file.Path;
@@ -45,9 +46,9 @@ public record ArweaveBlocksConfig(
}
}
public static final String TEST_TAG_NAME = "App";
public static final String TEST_TAG_VALUE = "test5590";
public static final String CHANNEL_TAG_NAME = "c_test5590";
public static final String TEST_TAG_NAME = ShineProtocolTags.APP_TAG_NAME;
public static final String TEST_TAG_VALUE = ShineProtocolTags.APP_TAG_VALUE;
public static final String CHANNEL_TAG_NAME = ShineProtocolTags.CHANNEL_TAG_NAME;
public static ArweaveBlocksConfig load() {
AppConfig c = AppConfig.getInstance();
@@ -29,7 +29,7 @@ final class Ans104BundleTest {
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("bundle-" + payload);
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
List<Ans104DataItem.Tag> tags = List.of(new Ans104DataItem.Tag("App", "test5590"));
List<Ans104DataItem.Tag> tags = List.of(new Ans104DataItem.Tag(blockchain.ShineProtocolTags.APP_TAG_NAME, blockchain.ShineProtocolTags.APP_TAG_VALUE));
byte[] signature = Ed25519Util.sign(Ans104DataItem.buildSigningMessage(owner, tags, data), privateKey);
return Ans104DataItem.serializeEd25519(signature, owner, tags, data);
}
@@ -17,6 +17,7 @@ import java.util.Objects;
* SHiNE Frame v1 (all integer fields BigEndian):
* [2] frameCode = 0x0001
* [32] prevHash32 = SHA-256(Frame v1 bytes of previous SHiNE block)
* [32] back32Hash32 = SHA-256(Frame v1 bytes of block N-32), or zero for N<32
* [4] blockSize = exact Frame v1 byte length
* [4] blockNumber
* [8] timestamp unix seconds
@@ -33,10 +34,11 @@ public final class BchBlockEntry {
public static final int FRAME_CODE_V1 = 0x0001;
public static final int MAX_BLOCK_FULL_BYTES = Ans104DataItem.MAX_DATA_ITEM_BYTES;
public static final long MAX_FUTURE_SECONDS = 60;
public static final int FRAME_HEADER_SIZE = 2 + 32 + 4 + 4 + 8 + 2 + 2 + 2;
public static final int FRAME_HEADER_SIZE = 2 + 32 + 32 + 4 + 4 + 8 + 2 + 2 + 2;
public final int frameCode;
public final byte[] prevHash32;
public final byte[] back32Hash32;
public final int blockSize;
public final int blockNumber;
public final long timestamp;
@@ -66,6 +68,8 @@ public final class BchBlockEntry {
}
this.prevHash32 = new byte[32];
bb.get(prevHash32);
this.back32Hash32 = new byte[32];
bb.get(back32Hash32);
this.blockSize = bb.getInt();
if (blockSize != frameBytes.length || blockSize < FRAME_HEADER_SIZE) {
throw new IllegalArgumentException("SHiNE blockSize mismatch: declared=" + blockSize + " actual=" + frameBytes.length);
@@ -0,0 +1,17 @@
package blockchain;
/**
* Канонические ANS-104 tag names/value текущего SHiNE transport namespace на сервере.
*
* При смене тестовой эпохи namespace меняется отдельно в трёх рабочих местах:
* сервер, UI и актуальный Arweave Viewer.
*/
public final class ShineProtocolTags {
private ShineProtocolTags() {}
public static final String NAMESPACE = "test5591";
public static final String APP_TAG_NAME = "App";
public static final String APP_TAG_VALUE = NAMESPACE;
public static final String CHANNEL_TAG_NAME = "c";
public static final String TYPE_TAG_NAME = "t";
}
@@ -15,8 +15,8 @@ final class Ans104DataItemTest {
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
List<Ans104DataItem.Tag> tags = List.of(
new Ans104DataItem.Tag("App", "test5590"),
new Ans104DataItem.Tag("c_test5590", "books")
new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE),
new Ans104DataItem.Tag(ShineProtocolTags.CHANNEL_TAG_NAME, "books")
);
byte[] message = Ans104DataItem.buildSigningMessage(owner, tags, data);
@@ -27,8 +27,8 @@ final class Ans104DataItemTest {
assertEquals(2, parsed.signatureType());
assertArrayEquals(owner, parsed.owner32());
assertArrayEquals(data, parsed.data());
assertTrue(parsed.hasTag("App", "test5590"));
assertEquals("books", parsed.tagValue("c_test5590"));
assertTrue(parsed.hasTag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE));
assertEquals("books", parsed.tagValue(ShineProtocolTags.CHANNEL_TAG_NAME));
assertTrue(parsed.verifySignature());
assertEquals(32, parsed.id32().length);
}
@@ -38,7 +38,7 @@ final class Ans104DataItemTest {
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("shine-ans104-mutation-key");
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
byte[] data = "original".getBytes(StandardCharsets.UTF_8);
List<Ans104DataItem.Tag> tags = List.of(new Ans104DataItem.Tag("App", "test5590"));
List<Ans104DataItem.Tag> tags = List.of(new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE));
byte[] signature = Ed25519Util.sign(Ans104DataItem.buildSigningMessage(owner, tags, data), privateKey);
byte[] raw = Ans104DataItem.serializeEd25519(signature, owner, tags, data);
raw[raw.length - 1] ^= 1;
@@ -50,8 +50,8 @@ final class Ans104DataItemTest {
for (int i = 0; i < owner.length; i++) owner[i] = (byte) i;
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
List<Ans104DataItem.Tag> tags = List.of(
new Ans104DataItem.Tag("App", "test5590"),
new Ans104DataItem.Tag("c_test5590", "books")
new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE),
new Ans104DataItem.Tag(ShineProtocolTags.CHANNEL_TAG_NAME, "books")
);
byte[] actual = Ans104DataItem.buildSigningMessage(owner, tags, data);
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.MsgSubType;
import blockchain.ShineProtocolTags;
import blockchain.body.BodyHasLine;
import blockchain.body.BodyHasTarget;
import blockchain.body.ConnectionBody;
@@ -203,6 +204,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
case "bad_block_number" -> "Некорректный номер блока";
case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
case "bad_back32_hash" -> "Некорректный back32Hash (резервная ссылка N-32 не совпадает)";
case "db_error_back32_check" -> "Ошибка БД при проверке back32Hash";
case "bad_blockchain_key_len" -> "Некорректный ключ блокчейна в состоянии (ожидалось 32 байта)";
case "signature_verify_failed" -> "Ошибка проверки подписи блока";
case "bad_signature" -> "Некорректная подпись блока";
@@ -316,13 +319,13 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
}
if (!block.getDataItem().hasTag("App", "test5590")) {
if (!block.getDataItem().hasTag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE)) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_app_tag", serverLastNum, serverLastHashHex);
}
// Signed secondary index for the fixed SHiNE streams.
String expectedTypeTag = expectedTypeTag(block);
String actualTypeTag = block.getDataItem().tagValue("t");
String actualTypeTag = block.getDataItem().tagValue(ShineProtocolTags.TYPE_TAG_NAME);
if (expectedTypeTag != null) {
if (!expectedTypeTag.equals(actualTypeTag)) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_type_tag", serverLastNum, serverLastHashHex);
@@ -472,10 +475,10 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
channelMetaUpdateEntry.setMetaUpdatedAtMs(block.timestamp * 1000L);
}
// Channel DataItems are indexed by a signed canonical channel slug tag: c_test5590=<slug>.
// Channel DataItems are indexed by a signed canonical channel slug tag: c=<slug>.
try {
String expectedChannelSlug = expectedChannelSlug(blockchainName, block, channelNameStateEntry);
String actualChannelSlug = block.getDataItem().tagValue("c_test5590");
String actualChannelSlug = block.getDataItem().tagValue(ShineProtocolTags.CHANNEL_TAG_NAME);
if (expectedChannelSlug != null) {
if (!expectedChannelSlug.equals(actualChannelSlug)) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_tag", serverLastNum, serverLastHashHex);
@@ -551,6 +554,29 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
}
// 4.4) резервная глобальная ссылка: N<32 -> ZERO32, иначе hash блока N-32.
try {
byte[] expectedBack32 = new byte[32];
if (block.blockNumber >= 32) {
byte[] storedBack32 = blocksDAO.getHashByNumber(blockchainName, block.blockNumber - 32);
if (storedBack32 == null || storedBack32.length != 32) {
log.error("AddBlock: missing N-32 block for back32 validation (blockchainName={}, blockNumber={}, target={})",
blockchainName, block.blockNumber, block.blockNumber - 32);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_back32_check", serverLastNum, serverLastHashHex);
}
expectedBack32 = storedBack32;
}
if (!Arrays.equals(block.back32Hash32, expectedBack32)) {
log.warn("AddBlock: bad_back32_hash (login={}, blockchainName={}, blockNumber={}, clientBack32={}, expectedBack32={})",
login, blockchainName, block.blockNumber, toHex(block.back32Hash32), toHex(expectedBack32));
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_back32_hash", serverLastNum, serverLastHashHex);
}
} catch (Exception e) {
log.error("AddBlock: db_error_back32_check (login={}, blockchainName={}, blockNumber={})",
login, blockchainName, block.blockNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_back32_check", serverLastNum, serverLastHashHex);
}
// 5) pubKey
final byte[] pubKey32 = st.getBlockchainKeyBytes();
if (pubKey32 == null || pubKey32.length != 32) {
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.keyRotation;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.MsgSubType;
import blockchain.ShineProtocolTags;
import blockchain.body.ForkBody;
import server.logic.ws_protocol.Base64Ws;
import server.logic.ws_protocol.JSON.ConnectionContext;
@@ -98,17 +99,28 @@ public final class Net_KeyRotationAddBlock_Handler implements JsonMessageHandler
if (block.blockNumber != req.getBlockNumber()) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BLOCK_NUMBER_MISMATCH", "blockNumber запроса не совпадает с блоком");
}
if (!block.getDataItem().hasTag("App", "test5590")) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_APP_TAG", "Отсутствует App=test5590");
if (!block.getDataItem().hasTag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE)) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_APP_TAG", "Отсутствует App=" + ShineProtocolTags.APP_TAG_VALUE);
}
String expectedTypeTag = expectedTypeTag(block);
String actualTypeTag = block.getDataItem().tagValue("t");
String actualTypeTag = block.getDataItem().tagValue(ShineProtocolTags.TYPE_TAG_NAME);
if (expectedTypeTag != null ? !expectedTypeTag.equals(actualTypeTag) : actualTypeTag != null) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_TYPE_TAG", "Некорректный t-тег candidate DataItem");
}
if (!requestPrevHashMatches(req.getPrevBlockHash(), block.prevHash32)) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_PREV_HASH_MISMATCH", "prevBlockHash запроса не совпадает с блоком");
}
byte[] expectedBack32 = new byte[32];
if (block.blockNumber >= 32) {
KeyRotationCandidateBlockEntry back = candidates.getByNumber(c, session.getId(), block.blockNumber - 32);
if (back == null || back.getBlockHash() == null || back.getBlockHash().length != 32) {
return NetExceptionResponseFactory.error(req, 409, "KEY_ROTATION_BACK32_SOURCE_MISSING", "Нет candidate-блока N-32 для проверки back32Hash");
}
expectedBack32 = back.getBlockHash();
}
if (!Arrays.equals(block.back32Hash32, expectedBack32)) {
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BACK32_HASH_MISMATCH", "back32Hash candidate-блока не совпадает с N-32");
}
byte[] newBlockchainKey;
try {
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.keyRotation;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.MsgSubType;
import blockchain.ShineProtocolTags;
import blockchain.body.ForkBody;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
@@ -147,9 +148,9 @@ public final class Net_KeyRotationFinishChain_Handler implements JsonMessageHand
if (candidate.blockNumber != i) return "KEY_ROTATION_BLOCK_NUMBER_MISMATCH";
if (!Arrays.equals(candidate.getHash32(), stored.getBlockHash())) return "KEY_ROTATION_STORED_HASH_MISMATCH";
if (!Arrays.equals(candidate.getDataItemId32(), stored.getDataItemId())) return "KEY_ROTATION_STORED_DATAITEM_MISMATCH";
if (!candidate.getDataItem().hasTag("App", "test5590")) return "KEY_ROTATION_BAD_APP_TAG";
if (!candidate.getDataItem().hasTag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE)) return "KEY_ROTATION_BAD_APP_TAG";
String expectedTypeTag = expectedTypeTag(candidate);
String actualTypeTag = candidate.getDataItem().tagValue("t");
String actualTypeTag = candidate.getDataItem().tagValue(ShineProtocolTags.TYPE_TAG_NAME);
if (expectedTypeTag != null ? !expectedTypeTag.equals(actualTypeTag) : actualTypeTag != null) return "KEY_ROTATION_BAD_TYPE_TAG";
if (!BchCryptoVerifier.verifyBlock(candidate, newBlockchainKey)) return "KEY_ROTATION_BAD_SIGNATURE";
@@ -160,6 +161,14 @@ public final class Net_KeyRotationFinishChain_Handler implements JsonMessageHand
return "KEY_ROTATION_CHAIN_HASH_MISMATCH";
}
}
if (i < 32) {
if (!isZero32(candidate.back32Hash32)) return "KEY_ROTATION_BACK32_HASH_MISMATCH";
} else {
byte[] expectedBack32 = entries.get(i - 32).getBlockHash();
if (expectedBack32 == null || !Arrays.equals(candidate.back32Hash32, expectedBack32)) {
return "KEY_ROTATION_BACK32_HASH_MISMATCH";
}
}
String semanticError = validateAgainstRotation(c, session, candidate);
if (semanticError != null) return semanticError;
@@ -137,7 +137,7 @@ test.freeAvatar.walletJwkPath=
# ============================================================
# Arweave per-user-block transport (ANS-104)
# Test namespace: each user DataItem is signed with App=test5590.
# Test namespace: each user DataItem is signed with App=test5591.
# Channel DataItems additionally contain c=<canonical-channel-slug>.
# ============================================================
arweave.blocks.publish.enabled=false
@@ -2,6 +2,7 @@ package test.it.blockchain;
import blockchain.Ans104DataItem;
import blockchain.BchBlockEntry;
import blockchain.ShineProtocolTags;
import blockchain.body.*;
import test.it.utils.TestConfig;
import test.it.utils.TestIds;
@@ -68,6 +69,7 @@ public final class AddBlockSender {
int blockNumber = state.nextBlockNumber();
byte[] prevHash32 = state.prevHash32ForNext();
byte[] back32Hash32 = state.back32Hash32ForNext();
long tsSec = System.currentTimeMillis() / 1000L;
short type = typeOf(body);
@@ -76,15 +78,15 @@ public final class AddBlockSender {
byte[] bodyBytes = body.toBytes();
byte[] frame = buildFrame(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);
byte[] frame = buildFrame(prevHash32, back32Hash32, blockNumber, tsSec, type, subType, version, bodyBytes);
byte[] owner32 = utils.crypto.Ed25519Util.derivePublicKey(loginPrivKey);
List<Ans104DataItem.Tag> tags = new ArrayList<>();
tags.add(new Ans104DataItem.Tag("App", "test5590"));
tags.add(new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE));
String typeTag = typeTagFor(type);
if (typeTag != null) tags.add(new Ans104DataItem.Tag("t", typeTag));
if (typeTag != null) tags.add(new Ans104DataItem.Tag(ShineProtocolTags.TYPE_TAG_NAME, typeTag));
String channelSlug = channelSlugFor(body);
if (channelSlug != null) tags.add(new Ans104DataItem.Tag("c_test5590", channelSlug));
if (channelSlug != null) tags.add(new Ans104DataItem.Tag(ShineProtocolTags.CHANNEL_TAG_NAME, channelSlug));
byte[] signingMessage = Ans104DataItem.buildSigningMessage(owner32, tags, frame);
byte[] signature64 = utils.crypto.Ed25519Util.sign(signingMessage, loginPrivKey);
@@ -232,6 +234,7 @@ public final class AddBlockSender {
}
private static byte[] buildFrame(byte[] prevHash32,
byte[] back32Hash32,
int blockNumber,
long tsSec,
short type,
@@ -242,12 +245,16 @@ public final class AddBlockSender {
if (prevHash32 == null || prevHash32.length != 32) {
throw new IllegalArgumentException("prevHash32 must be 32 bytes");
}
if (back32Hash32 == null || back32Hash32.length != 32) {
throw new IllegalArgumentException("back32Hash32 must be 32 bytes");
}
int bodyLen = bodyBytes == null ? 0 : bodyBytes.length;
int blockSize = BchBlockEntry.FRAME_HEADER_SIZE + bodyLen;
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
bb.putShort((short) BchBlockEntry.FRAME_CODE_V1);
bb.put(prevHash32);
bb.put(back32Hash32);
bb.putInt(blockSize);
bb.putInt(blockNumber);
bb.putLong(tsSec);
@@ -123,10 +123,18 @@ public final class ChainState {
}
public byte[] prevHash32ForNext() {
if (lastBlockNumber < 0) return ZERO32;
if (lastBlockNumber < 0) return ZERO32.clone();
return hexToBytes32(lastBlockHashHex);
}
public byte[] back32Hash32ForNext() {
int next = nextBlockNumber();
if (next < 32) return ZERO32.clone();
byte[] h = hash32ByNumber.get(next - 32);
if (h == null || h.length != 32) throw new IllegalStateException("Missing hash for block " + (next - 32));
return h.clone();
}
public byte[] headerHash32() {
return headerHash32 == null ? null : headerHash32.clone();
}
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.14.5
server.version=1.12.3
client.version=1.14.6
server.version=1.12.4
+3 -2
View File
@@ -22,6 +22,7 @@
- `blockchainName` — целевая пользовательская chain;
- `blockNumber` — должен совпадать с номером внутри Frame v1 и быть `serverLast + 1`;
- `prevBlockHash` — 32-byte SHiNE hash предыдущего Frame в hex; должен совпадать с `prevHash32` внутри Frame и серверной вершиной;
- внутри Frame также обязателен `back32Hash32`: нули для blockNumber < 32, иначе hash Frame блока `blockNumber-32`;
- `blockBytesB64` — **полный serialized ANS-104 DataItem** в Base64.
Старый формат `preimage + sigMarker + signature` не поддерживается.
@@ -31,13 +32,13 @@
- generic Ed25519 signature type `2`;
- Ed25519 signature 64 bytes;
- owner 32 bytes и равен текущему blockchain public key;
- обязательный тестовый tag `App=test5590`;
- обязательный тестовый tag `App=test5591`;
- для channel block — `c=<canonical_channel_slug>`;
- `data` содержит SHiNE Frame v1 (`frameCode=1`).
## Основные ошибки
- `bad_app_tag` — отсутствует/неверен `App=test5590`;
- `bad_app_tag` — отсутствует/неверен `App=test5591`;
- `bad_channel_tag` — `c` отсутствует, лишний или не совпадает с canonical slug;
- `bad_signature` / `signature_verify_failed` — DataItem не подписан текущим blockchain key;
- `bad_block_number` — нарушена последовательность;
+10 -7
View File
@@ -27,6 +27,7 @@ ANS-104 DataItem
|---|---:|---|
| `frameCode` | 2 | `0x0001` |
| `prevHash32` | 32 | SHA-256 полного Frame v1 предыдущего SHiNE-блока; для блока 0 — нули |
| `back32Hash32` | 32 | SHA-256 Frame блока `N-32`; для `N<32` — 32 нулевых байта |
| `blockSize` | 4 | точный размер Frame v1, включая header и body |
| `blockNumber` | 4 | номер блока, начиная с 0 |
| `timestamp` | 8 | Unix time seconds |
@@ -35,11 +36,12 @@ ANS-104 DataItem
| `version` | 2 | версия body |
| `body` | N | данные конкретного типа |
`FRAME_HEADER_SIZE = 56` bytes.
`FRAME_HEADER_SIZE = 88` bytes.
```text
blockHash32 = SHA256(FrameV1Bytes)
next.prevHash32 = blockHash32
blockN.back32Hash32 = (N < 32 ? ZERO_HASH_32 : HASH(block N-32))
```
Подписи внутри Frame v1 нет. Единственная подпись пользователя — подпись окружающего ANS-104 DataItem.
@@ -72,7 +74,7 @@ next.prevHash32 = blockHash32
Каждый блок:
```text
App = test5590
App = test5591
```
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
@@ -94,7 +96,7 @@ t=prof # USER_PROFILE
Если блок относится к конкретному каналу, он дополнительно содержит:
```text
c_test5590 = <canonical_channel_slug>
c = <canonical_channel_slug>
```
Slug входит в подпись DataItem и не может быть изменён сервером после подписи.
@@ -118,16 +120,17 @@ Slug входит в подпись DataItem и не может быть изм
Сервер обязан:
1. распарсить полный ANS-104 DataItem;
2. проверить `App=test5590`;
2. проверить `App=test5591`;
3. проверить `t`, если тип блока требует фиксированный индексный поток;
4. проверить `c_test5590`, если тип блока требует канал;
4. проверить `c`, если тип блока требует канал;
5. проверить ANS-104 Ed25519 подпись;
6. проверить, что `owner` равен текущему blockchain public key пользователя;
7. распарсить Frame v1 и body;
8. проверить `blockNumber == last + 1`;
9. проверить `prevHash32 == lastBlockHash`;
10. проверить строгую непрерывность логической линии, если body line-based;
11. записать DataItem и новое состояние атомарно в PostgreSQL.
10. проверить `back32Hash32`: нули для `N<32`, иначе hash блока `N-32`;
11. проверить строгую непрерывность логической линии, если body line-based;
12. записать DataItem и новое состояние атомарно в PostgreSQL.
## Нормативное правило версий body
@@ -10,7 +10,7 @@
## 2. Логические линии внутри пользовательской цепочки
Глобально все блоки пользователя идут одной последовательностью `blockNumber` и `prevHash32`. Дополнительно некоторые типы образуют собственные проверяемые линии.
Глобально все блоки пользователя идут одной последовательностью `blockNumber` и `prevHash32`; каждый Frame также содержит резервный `back32Hash32` на блок `N-32` (нули для `N<32`). Дополнительно некоторые типы образуют собственные проверяемые линии. Отдельного back32 внутри линий нет.
Актуальные линии:
+1 -1
View File
@@ -25,7 +25,7 @@ HEADER сам не хранит line-prefix, но является root TECH-л
- является очередным шагом TECH-линии;
- является root нового канала;
- имеет `t=tech`;
- имеет `c_test5590=<canonical_channel_slug>`.
- имеет `c=<canonical_channel_slug>`.
Начало body:
@@ -1,5 +1,7 @@
# ANS-104 / Arweave transport для пользовательских блоков SHiNE
> **Текущий transport namespace:** `test5591`. Он намеренно хранится отдельно в трёх рабочих местах: сервер (`ShineProtocolTags.java`), UI (`js/services/protocol-tags.js`) и актуальный Viewer (`static-sites/arweave-viewer/index.html`). При смене тестовой эпохи эти три значения меняются синхронно. Имя канального индексного тега постоянно и не зависит от namespace: `c`.
## Цель
Каждый пользовательский блок SHiNE уже на клиенте является самостоятельным подписанным ANS-104 DataItem. Сервер проверяет и хранит **точно эти signed bytes** и может публиковать их одним из двух транспортов: через Turbo по одному DataItem либо через прямую Arweave L1-транзакцию в составе стандартного большого ANS-104 bundle.
@@ -11,7 +13,7 @@
Для тестового контура обязательно:
```text
App=test5590
App=test5591
```
Для фиксированных потоков:
@@ -25,10 +27,10 @@ t=prof # USER_PROFILE
Для блоков конкретного канала дополнительно:
```text
c_test5590=<canonical_channel_slug>
c=<canonical_channel_slug>
```
`TECH_CREATE_CHANNEL` несёт оба индекса: `t=tech` и `c_test5590=<slug>`. `STATUS_ACTION` не получает `t`-тег.
`TECH_CREATE_CHANNEL` несёт оба индекса: `t=tech` и `c=<slug>`. `STATUS_ACTION` не получает `t`-тег.
Теги входят в ANS-104 подпись пользователя. Сервер проверяет соответствие `t` фактическому `msg_type` и отклоняет лишний/неверный `t`. Старый тестовый тег `c` новым кодом не создаётся и не принимается как channel tag.
@@ -92,7 +94,7 @@ Bundle-Version=2.0.0
Content-Type=application/octet-stream
```
Специальный `App=test5590-batch` больше не используется. Важны вложенные user DataItems, у которых уже есть `App=test5590`.
Специальный `App=test5591-batch` больше не используется. Важны вложенные user DataItems, у которых уже есть `App=test5591`.
### `none`
@@ -112,7 +114,7 @@ Content-Type=application/octet-stream
Importer всегда выполняет один discovery-запрос:
```text
App=test5590
App=test5591
```
Он **не ищет root bundles** и не зависит от `publish.mode`.
@@ -130,7 +132,7 @@ App=test5590
Поэтому importer:
1. получает `data_item_id` через GraphQL `App=test5590`;
1. получает `data_item_id` через GraphQL `App=test5591`;
2. запрашивает `GET /ar-io/offsets/{data_item_id}`;
3. получает `rootTxId`, `rootOffset`, `size`;
4. делает range-read `GET /raw/{rootTxId}` ровно по этому диапазону;
+12 -2
View File
@@ -1,3 +1,13 @@
## 2026-10-02 — Transport namespace `test5591`
- Текущий ANS-104 namespace переключён с `test5590` на `test5591`: `App=test5591`; channel index теперь постоянный и не зависит от namespace: `c=<slug>`.
- Namespace хранится отдельно в трёх рабочих местах: сервер, UI и актуальный `shine-UI/static-sites/arweave-viewer`; это намеренная простая конфигурация без cross-module runtime-зависимости.
- Сервер, UI и актуальный Viewer используют свои локальные константы namespace; общего cross-module `protocol-config` нет.
- Серверные AddBlock/key-rotation/archive-sync проверки больше не содержат захардкоженного `test5591`.
- Исторические упоминания `test5590` в старых changelog/legacy patch-документах оставлены как история предыдущего тестового namespace.
- Изменение рассчитано на новую чистую тестовую БД/цепочки; старые `App=test5590` DataItem новым namespace автоматически не подхватываются.
- Frame header увеличен с 56 до 88 bytes из-за `back32Hash32`; сервер проверяет N-32, Viewer показывает и проверяет резервную ссылку.
## 2026-10-01 — Проверяемые USER_PROFILE / CONNECTION / TECH линии и signed `t` index
- Тип `4` в документации называется `USER_PROFILE`; внутренние исторические имена классов могут сохраняться. USER_PROFILE предназначен только для данных профиля.
@@ -5,8 +15,8 @@
- `TECH_FORK` включён в общую TECH-линию вместе с `TECH_CREATE_CHANNEL`; оба используют `lineCode=0`, предыдущий TECH block/hash и `techSequence`.
- Серверная line-валидация стала строгой: новый блок обязан продолжать фактический текущий хвост той же линии и иметь sequence `+1`; произвольная ссылка на существующий блок больше не принимается. Это также устраняет боковую ветку канала после `EDIT_POST`.
- Добавлены подписанные ANS-104 индексы: `t=prof`, `t=conn`, `t=tech`. `STATUS_ACTION` не изменён.
- В отдельном `shine-solana-arweave-viewer-v3` исправлен legacy-фильтр канала `c` → канонический `c_test5590`.
- `back32Hash` и дополнительные skip-ссылки не добавлялись.
- Единственным актуальным Viewer считается `shine-UI/static-sites/arweave-viewer`; старые корневые Viewer перенесены в его `history/legacy-root-viewers/`.
- В общий Frame добавлен глобальный `back32Hash32`: для блоков `#0..#31` нули, начиная с `#32` — SHA-256 Frame блока `N-32`. Отдельный back32 для line-body не добавляется.
- Формат USER_PROFILE/CONNECTION/TECH_FORK несовместим с предыдущей тестовой схемой; проект находится до production и миграция старых блоков не предусмотрена.
## 2026-09-27 — TECH_FORK v1
@@ -1,3 +1,5 @@
> **LEGACY / historical patch note.** Current protocol uses `App=test5591` and the permanent channel tag `c=<slug>`. Do not apply the `c_test5590` instructions below to current code.
# Применение patch: Turbo + direct Arweave для `App=test5590`
## Что меняется
+3 -3
View File
@@ -14,7 +14,7 @@
```text
User
-> создаёт Frame v1
-> tags: App=test5590; t=tech/conn/prof для фиксированных потоков; при канале c_test5590=<slug>
-> tags: App=test5591; t=tech/conn/prof для фиксированных потоков; при канале c=<slug>
-> Ed25519 подписывает ANS-104 deep-hash
-> готовый DataItem
-> AddBlock
@@ -27,7 +27,7 @@ Server
ИЛИ publish.mode=none: наружу не публиковать
Other servers
-> GraphQL App=test5590
-> GraphQL App=test5591
-> скачивают/извлекают DataItem
-> verify
-> PostgreSQL без повторной публикации
@@ -38,4 +38,4 @@ Other servers
- SHiNE `prevHash` остаётся SHA-256 предыдущего Frame v1 и не зависит от Arweave.
- `data_item_id` нужен для Arweave, поиска и дедупликации.
- PostgreSQL — единственное локальное хранилище пользовательских блоков.
- Тестовый namespace `App=test5590` специально отделён от будущего production namespace.
- Тестовый namespace `App=test5591` специально отделён от будущего production namespace.
+20 -172
View File
@@ -1,28 +1,30 @@
# TODO — резервные ссылки `back32Hash` для SHiNE blockchain
# `back32Hash32` — глобальная резервная ссылка SHiNE
Статус: **TODO / не реализовано**.
Статус: **реализовано для общей пользовательской цепочки**. Отдельный `back32` для логических линий намеренно не добавляется.
Цель этой идеи — повысить устойчивость проверки SHiNE blockchain и отдельных логических линий к ситуации, когда один или несколько DataItem временно или постоянно недоступны у конкретного Arweave gateway, CDN, сервера или другого источника.
## Правило
Это **не замена** обычным ссылкам на предыдущий блок. Основная цепочка по-прежнему строится через `prevHash32` / `prevLineHash32`. `back32...` — только дополнительная резервная ссылка назад.
---
## 1. Глобальная цепочка пользователя
В общий Frame в будущем добавить одно поле:
Каждый SHiNE Frame содержит:
```text
[32] prevHash32
[32] back32Hash32
```
Смысл:
`prevHash32` остаётся основной соседней связью `N -> N-1`.
`back32Hash32` — дополнительная резервная связь:
```text
back32Hash32 = HASH(global block N - 32)
N < 32 -> ZERO_HASH_32
N >= 32 -> SHA256(Frame блока N-32)
```
То есть блок `N` дополнительно содержит hash глобального блока `N-32`.
Номер блока для этой ссылки отдельно не хранится: он всегда равен `blockNumber - 32`. Поэтому дополнительных 4 байта номера нет. Стоимость механизма — **ровно +32 байта на каждый глобальный блок**.
## Зачем
Если соседний DataItem недоступен у конкретного gateway/CDN/сервера, обычная проверка через `prevHash32` прерывается. `back32Hash32` даёт второй криптографический мост к более ранней части истории. Это не восстанавливает отсутствующий блок и не доказывает его содержимое, но позволяет связать доступный последующий участок с историей на 32 блока раньше.
Пример:
@@ -32,172 +34,18 @@ block #100
back32Hash32 -> HASH(#68)
```
### Для первых блоков
Если блока `N-32` ещё не существует, поле содержит 32 нулевых байта:
Для первых 32 блоков:
```text
#0 ... #31 -> back32Hash32 = ZERO_HASH_32
#32 -> HASH(#0)
#33 -> HASH(#1)
...
```
### Номер блока отдельно НЕ хранить
Сервер при `AddBlock` обязан сам проверить это поле по сохранённому блоку `N-32`; клиентское значение не принимается на доверии.
Поле вида `back32BlockNumber` не нужно.
## Почему без `back32` внутри линий
Номер однозначно вычисляется:
CHANNEL / TECH / CONNECTION / USER_PROFILE уже имеют собственные `prev...BlockNumber`, `prev...Hash32` и sequence. Эти линии — дополнительный способ выборочной загрузки и проверки поверх основной пользовательской цепочки. Если в выборочно скачанной линии обнаружена дырка, блоки всё равно можно криптографически проверить через глобальную цепочку, где уже есть `prevHash32 + back32Hash32`.
```text
back32BlockNumber = blockNumber - 32
```
Поэтому хранить ещё 4 байта номера в каждом блоке бессмысленно.
Дополнительный размер глобального блока: **ровно +32 байта**.
---
## 2. Логические линии
Для line-based блоков в будущем добавить ещё одно поле:
```text
[32] back32LineHash32
```
Оно относится не к глобальной последовательности блоков, а к конкретной логической линии.
Используется для:
- `TEXT` — линия конкретного канала;
- `TECH` — техническая линия пользователя;
- `CONNECTION` — линия изменений связей;
- `USER_PROFILE` — линия изменений профиля.
Смысл:
```text
back32LineHash32 = HASH(line item with sequence = currentSequence - 32)
```
Пример для канала:
```text
channel sequence 100
prevLineHash32 -> HASH(channel sequence 99)
back32LineHash32 -> HASH(channel sequence 68)
```
Пример для профиля:
```text
profileSequence = 50
prevProfileBlockHash32 -> HASH(profile sequence 49)
back32LineHash32 -> HASH(profile sequence 18)
```
Если элемента линии с `sequence - 32` ещё нет, используется:
```text
ZERO_HASH_32
```
### Номер элемента `-32` отдельно НЕ хранить
Он вычисляется из текущего sequence:
```text
back32LineSequence = currentSequence - 32
```
А глобальный номер нужного старого блока при необходимости определяется по уже загруженной/проиндексированной линии.
Дополнительный размер line-based блока:
```text
+32 байта global back32Hash32
+32 байта back32LineHash32
= +64 байта
```
---
## 3. Зачем это нужно
Обычная цепочка имеет только соседнюю связь:
```text
N -> N-1
```
Если блок `N-1` недоступен, проверить обычную связь следующего блока назад уже невозможно, хотя остальные данные могут быть доступны.
С дополнительной ссылкой получается:
```text
N -> N-1
N -> N-32
```
Поэтому даже если часть соседних блоков недоступна, можно получить дополнительную криптографическую связь с более ранней частью истории.
То же самое для отдельной линии:
```text
line N -> line N-1
line N -> line N-32
```
Это особенно полезно для каналов: если конкретный пост оказался недоступен или заблокирован одним источником, отсутствие этого DataItem не должно автоматически делать всю последующую историю канала полностью непроверяемой.
---
## 4. Что этот механизм НЕ решает
`back32Hash` не означает, что отсутствующий блок восстановлен или проверен.
Если блока нет, его содержимое остаётся неизвестным.
Механизм даёт только дополнительный проверяемый мост между доступными частями истории.
Он также не заменяет:
- `prevHash32`;
- `prevLineHash32`;
- sequence логической линии;
- подпись ANS-104 DataItem;
- проверку текущего хвоста линии;
- отдельный механизм доказательства того, что скачан самый последний существующий блок/элемент линии.
---
## 5. Почему шаг именно 32
Шаг `32` выбран как простой компромисс:
- всего 32 дополнительных байта на глобальную цепочку;
- ещё 32 байта только для line-based блоков;
- позволяет делать длинный резервный переход без сложной структуры skip-list;
- номер ссылки не надо хранить — он вычисляется арифметически;
- реализация и проверка остаются очень простыми.
В будущем шаг можно пересмотреть только через новую версию Frame/body, если появится убедительная причина.
---
## 6. План реализации в будущем
Когда будет принято решение реализовать TODO:
1. добавить `back32Hash32` в новую версию общего Frame;
2. при создании блока вычислять hash глобального блока `N-32`;
3. добавить `back32LineHash32` в новые версии всех line-based body;
4. вычислять его по элементу той же линии с sequence `currentSequence-32`;
5. серверу проверять эти hashes при `AddBlock`;
6. импортерам/Arweave sync/viewer использовать резервные ссылки при проверке частично доступной истории;
7. обновить тесты бинарного формата и документацию;
8. не добавлять отдельные поля номера для ссылок `-32`.
До выполнения этих пунктов текущий канонический формат SHiNE остаётся без `back32Hash32` и `back32LineHash32`.
Поэтому добавлять ещё +32 байта в каждый line-based body сейчас нецелесообразно. Если когда-нибудь появится требование полностью проверять отдельный канал/профиль без доступа к глобальной цепочке даже через недоступный line-блок, line-back32 можно рассмотреть отдельной новой версией body.
+1 -1
View File
@@ -40,7 +40,7 @@ blockchain.sync.enabled=false
Дополнительно каждый сервер может независимо включить `ArweaveBlockSyncScheduler`.
Он ищет child DataItems по тестовому тегу `App=test5590`, проверяет их и импортирует через ту же бизнес-проверку AddBlock. Импортированные блоки не публикуются повторно.
Он ищет child DataItems по тестовому тегу `App=test5591`, проверяет их и импортирует через ту же бизнес-проверку AddBlock. Импортированные блоки не публикуются повторно.
Если блоки обнаружены не по порядку, persistent queue оставляет более поздние блоки pending до появления предыдущих.
+30 -26
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(),
if (!cleanBlockchainName || !Number.isInteger(cleanBlockNumber) || cleanBlockNumber < 0) {
throw new Error('Некорректный blockchainName/blockNumber для GetBlockchainBlock');
}
const response = await this.ws.request('GetBlockchainBlock', {
blockchainName: cleanBlockchainName,
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 (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');
-45
View File
@@ -1,45 +0,0 @@
# SHiNE Arweave Viewer v2 — ANS-104 / Frame v1
Новая версия viewer для формата, где каждый пользовательский блок хранится в Arweave как отдельный ANS-104 DataItem, а `data` DataItem содержит один `SHiNE Frame v1`.
Основной файл: `index.html`.
## Быстрый запуск
Через логин (ключ берётся из Solana PDA):
```text
index.html?login=aidarkc&channel=books
```
Напрямую через blockchain public key, без Solana:
```text
index.html?key=<BASE58_BLOCKCHAIN_PUBLIC_KEY>&channel=books
```
Дополнительные параметры:
```text
&arweaveGateway=https://arweave.net
&solanaRpc=https://api.mainnet-beta.solana.com
```
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5590` и строит список каналов из найденных блоков.
## Что делает 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` добавляет тег `c_test5590=<channel>`.
5. Загружает payload каждого DataItem через gateway `/<DataItemId>`.
6. Разбирает payload как SHiNE Frame v1 (`frameCode=0x0001`, header 56 bytes).
7. Дедуплицирует повторно опубликованные одинаковые пользовательские блоки по SHA-256 Frame v1.
8. Восстанавливает канал/сообщения тем же body parser, который использовал старый viewer.
## Важно
При фильтрации по конкретному `channel` viewer получает только блоки с соответствующим тегом `c_test5590`. Поэтому он не заявляет полную проверку глобальной `prevHash`-цепочки пользователя: между двумя блоками одного канала могут существовать блоки других каналов. SHA-256 каждого загруженного Frame вычисляется локально.
Старый `shine-solana-arweave-viewer/` не изменяется; новая версия лежит отдельно в `shine-solana-arweave-viewer-v2/`.
@@ -1,11 +0,0 @@
# SHiNE Solana Arweave Viewer
Основной рабочий файл:
- `index.html`
Подробное описание работы, параметров и готовых ссылок:
- `КАК_ЭТО_РАБОТАЕТ.md`
Viewer собран как автономный single-file HTML и запускается одним файлом без соседних зависимостей.
File diff suppressed because one or more lines are too long
@@ -1,114 +0,0 @@
# SHiNE Solana Arweave Viewer: как это работает
## Что лежит в папке
Основной и единственный рабочий файл viewer:
- `index.html`
Это автономный single-file viewer:
- весь HTML внутри одного файла;
- все стили внутри этого же файла;
- весь JavaScript внутри этого же файла;
- локальная копия `solana-web3` тоже встроена внутрь этого же файла.
Для запуска viewer достаточно открыть один `index.html`.
## Что делает viewer
Viewer открывается как обычная HTML-страница и работает полностью в браузере.
Он умеет:
- принимать логин пользователя SHiNE;
- принимать `Solana RPC` через форму или через URL-параметр;
- принимать `Arweave gateway` через форму или через URL-параметр;
- сохранять эти значения в `localStorage`;
- вычислять `user_pda` по логину для программы `shine_users`;
- читать `PDA` напрямую из `Solana`;
- распарсивать содержимое записи `shine_users`;
- показывать ключи, лимиты, номер записи, блокчейн пользователя и `Arweave tx id`;
- строить прямую ссылку на `Arweave`, если `tx id` есть в записи.
## Какие URL-параметры поддерживаются
Viewer понимает такие параметры:
- `login`
- `solanaRpc`
- `arweaveGateway`
Пример:
```text
index.html?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com&arweaveGateway=https%3A%2F%2Farweave.net
```
## Предустановленные адреса
### Solana RPC
- `https://solana-rpc.publicnode.com`
- `https://public.rpc.solanavibestation.com/`
### Arweave gateway
- `https://arweave.net`
## Почему одна из RPC может не работать
Адрес:
- `https://public.rpc.solanavibestation.com/`
может возвращать браузерную ошибку `403` с текстом про `Origin is not whitelisted`.
Это означает, что проблема обычно не в SHiNE viewer, а в CORS-политике самого RPC.
Для HTML-страницы, открытой прямо в браузере, это обычно не лечится со стороны клиента.
Практически для viewer лучше использовать:
- `https://solana-rpc.publicnode.com`
## Ссылки на загруженный файл в Arweave
Основная ссылка:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI`
Та же ссылка с тестовым логином `aidartest`:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest`
Та же ссылка с тестовым логином `aidartest` и явным рабочим RPC:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com`
## Что будет, если поставить devnet RPC
Официальный публичный `Solana devnet RPC`:
- `https://api.devnet.solana.com`
Если подставить `devnet` RPC, viewer начнёт читать именно сеть `devnet`, а не `mainnet`.
Но это не означает автоматически, что вся система SHiNE "стала тестовой".
Для реальной работы в `devnet` должны совпасть сразу несколько условий:
- программа `shine_users` должна быть задеплоена в `devnet`;
- в `devnet` должен существовать нужный `PDA` для логина;
- формат записи должен совпадать;
- данные пользователя должны реально лежать в этой тестовой сети.
Если этого нет, viewer просто не найдёт запись.
## Источник по devnet RPC
Официальная документация Solana:
- `https://solana.com/docs/references/clusters`
Там указан devnet endpoint:
- `https://api.devnet.solana.com`
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
# Как работает Viewer v2
Viewer рассчитан на новый архив SHiNE: **один пользовательский блок = один ANS-104 DataItem**.
### Режим 1: login + channel
`?login=<login>&channel=<slug>`
Viewer читает Solana PDA пользователя, получает `blockchainKey`, затем ищет Arweave DataItem этого владельца с тегами `App=test5590` и `c_test5590=<slug>`.
### Режим 2: key + channel
`?key=<base58 public key>&channel=<slug>`
Viewer пропускает Solana и сразу выполняет поиск в Arweave.
### Режим без channel
`?login=<login>` или `?key=<key>`
Viewer получает все доступные DataItem владельца с `App=test5590`, разбирает их и показывает восстановленные каналы.
### Новый блок
Payload DataItem:
```text
uint16be frameCode = 0x0001
bytes[32] prevHash = SHA-256(previous Frame v1)
int32be blockSize
int32be blockNumber
int64be timestamp seconds
uint16be type
uint16be subType
uint16be version
bytes[N] body
```
Подпись пользователя, owner и tags находятся в оболочке ANS-104 DataItem, а не в SHiNE Frame v1.
-57
View File
@@ -1,57 +0,0 @@
# SHiNE Arweave Viewer v2 — ANS-104 / Frame v1
Новая версия viewer для формата, где каждый пользовательский блок хранится в Arweave как отдельный ANS-104 DataItem, а `data` DataItem содержит один `SHiNE Frame v1`.
Основной файл: `index.html`.
## Быстрый запуск
Через логин (ключ берётся из Solana PDA):
```text
index.html?login=aidarkc&channel=books
```
Напрямую через blockchain public key, без Solana:
```text
index.html?key=<BASE58_BLOCKCHAIN_PUBLIC_KEY>&channel=books
```
Дополнительные параметры:
```text
&arweaveGateway=https://arweave.net
&solanaRpc=https://api.mainnet-beta.solana.com
```
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5590` и строит список каналов из найденных блоков.
## Что делает 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` добавляет тег `c_test5590=<channel>`.
5. Загружает payload каждого DataItem через gateway `/<DataItemId>`.
6. Разбирает payload как SHiNE Frame v1 (`frameCode=0x0001`, header 56 bytes).
7. Дедуплицирует повторно опубликованные одинаковые пользовательские блоки по SHA-256 Frame v1.
8. Восстанавливает канал/сообщения тем же body parser, который использовал старый viewer.
## Важно
При фильтрации по конкретному `channel` viewer получает только блоки с соответствующим тегом `c_test5590`. Поэтому он не заявляет полную проверку глобальной `prevHash`-цепочки пользователя: между двумя блоками одного канала могут существовать блоки других каналов. SHA-256 каждого загруженного Frame вычисляется локально.
Старый `shine-solana-arweave-viewer/` не изменяется; новая версия лежит отдельно в `shine-solana-arweave-viewer-v2/`.
## Проверка данных (v3.3)
Нижняя строка показывает краткий итог доступных проверок. Нажатие на неё или `⋮ → Проверка данных` открывает отдельный экран:
- локально вычисленный SHA-256 каждого SHiNE Frame;
- `prevHash` общей цепочки, когда предыдущий глобальный блок загружен;
- `prevLineHash` канальной цепочки, когда указанный предыдущий блок загружен;
- наличие DataItem в выборке Arweave owner соответствующего blockchain key;
- разрывы «нет данных» отдельно от доказанных несовпадений;
- честный статус ANS-104 signature: gateway payload/GraphQL не содержит сырой подписи, поэтому она не помечается как локально перепроверенная.
`○ нет данных` не является ошибкой. `⚠` появляется только при фактическом несовпадении доступных хэшей.
@@ -1,26 +0,0 @@
# SHiNE Arweave Viewer v3
Viewer нового per-action Arweave формата SHiNE.
## URL
- `?login=<login>&channel=<channel>` — получает blockchain key через Solana PDA, затем читает Arweave.
- `?key=<base58>&channel=<channel>` — Arweave запускается напрямую; Solana не является блокирующей зависимостью.
- без параметров — стартовая форма (логин или blockchain key + необязательный канал).
## v3
- переключатель `Канал / Блоки`;
- рекурсивные комментарии скрыты до раскрытия, на любой глубине;
- light/dark тема;
- меню `⋮`: кэш, Solana RPC, Arweave gateway;
- выключение кэша удаляет сохранённые данные каналов;
- одна нижняя status bar, по нажатию — подробный журнал;
- GraphQL читает от новых Arweave записей к старым (`HEIGHT_DESC`);
- до 6 параллельных загрузок Frame;
- отображение известных типов и raw-представление любых неизвестных блоков в режиме `Блоки`;
- лайки намеренно не отображаются.
Кэш использует browser localStorage и сериализует BigInt-поля Frame явно. Тема/RPC/gateway являются настройками и не удаляются при отключении кэша.
## V3.1 — streaming reverse loading
Arweave history is now processed page-by-page in `HEIGHT_DESC` order. The first GraphQL page (up to 100 DataItems) is downloaded, parsed and rendered immediately. Older pages are then fetched and merged in the background. The bottom status line shows the currently available block range while history expands toward the beginning. Each completed page is also persisted when cache is enabled.
@@ -1,11 +0,0 @@
# SHiNE Solana Arweave Viewer
Основной рабочий файл:
- `index.html`
Подробное описание работы, параметров и готовых ссылок:
- `КАК_ЭТО_РАБОТАЕТ.md`
Viewer собран как автономный single-file HTML и запускается одним файлом без соседних зависимостей.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-11
View File
@@ -1,11 +0,0 @@
# SHiNE Solana Arweave Viewer
Основной рабочий файл:
- `index.html`
Подробное описание работы, параметров и готовых ссылок:
- `КАК_ЭТО_РАБОТАЕТ.md`
Viewer собран как автономный single-file HTML и запускается одним файлом без соседних зависимостей.
@@ -1,11 +0,0 @@
# SHiNE Solana Arweave Viewer
Основной рабочий файл:
- `index.html`
Подробное описание работы, параметров и готовых ссылок:
- `КАК_ЭТО_РАБОТАЕТ.md`
Viewer собран как автономный single-file HTML и запускается одним файлом без соседних зависимостей.
File diff suppressed because one or more lines are too long
@@ -1,114 +0,0 @@
# SHiNE Solana Arweave Viewer: как это работает
## Что лежит в папке
Основной и единственный рабочий файл viewer:
- `index.html`
Это автономный single-file viewer:
- весь HTML внутри одного файла;
- все стили внутри этого же файла;
- весь JavaScript внутри этого же файла;
- локальная копия `solana-web3` тоже встроена внутрь этого же файла.
Для запуска viewer достаточно открыть один `index.html`.
## Что делает viewer
Viewer открывается как обычная HTML-страница и работает полностью в браузере.
Он умеет:
- принимать логин пользователя SHiNE;
- принимать `Solana RPC` через форму или через URL-параметр;
- принимать `Arweave gateway` через форму или через URL-параметр;
- сохранять эти значения в `localStorage`;
- вычислять `user_pda` по логину для программы `shine_users`;
- читать `PDA` напрямую из `Solana`;
- распарсивать содержимое записи `shine_users`;
- показывать ключи, лимиты, номер записи, блокчейн пользователя и `Arweave tx id`;
- строить прямую ссылку на `Arweave`, если `tx id` есть в записи.
## Какие URL-параметры поддерживаются
Viewer понимает такие параметры:
- `login`
- `solanaRpc`
- `arweaveGateway`
Пример:
```text
index.html?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com&arweaveGateway=https%3A%2F%2Farweave.net
```
## Предустановленные адреса
### Solana RPC
- `https://solana-rpc.publicnode.com`
- `https://public.rpc.solanavibestation.com/`
### Arweave gateway
- `https://arweave.net`
## Почему одна из RPC может не работать
Адрес:
- `https://public.rpc.solanavibestation.com/`
может возвращать браузерную ошибку `403` с текстом про `Origin is not whitelisted`.
Это означает, что проблема обычно не в SHiNE viewer, а в CORS-политике самого RPC.
Для HTML-страницы, открытой прямо в браузере, это обычно не лечится со стороны клиента.
Практически для viewer лучше использовать:
- `https://solana-rpc.publicnode.com`
## Ссылки на загруженный файл в Arweave
Основная ссылка:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI`
Та же ссылка с тестовым логином `aidartest`:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest`
Та же ссылка с тестовым логином `aidartest` и явным рабочим RPC:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com`
## Что будет, если поставить devnet RPC
Официальный публичный `Solana devnet RPC`:
- `https://api.devnet.solana.com`
Если подставить `devnet` RPC, viewer начнёт читать именно сеть `devnet`, а не `mainnet`.
Но это не означает автоматически, что вся система SHiNE "стала тестовой".
Для реальной работы в `devnet` должны совпасть сразу несколько условий:
- программа `shine_users` должна быть задеплоена в `devnet`;
- в `devnet` должен существовать нужный `PDA` для логина;
- формат записи должен совпадать;
- данные пользователя должны реально лежать в этой тестовой сети.
Если этого нет, viewer просто не найдёт запись.
## Источник по devnet RPC
Официальная документация Solana:
- `https://solana.com/docs/references/clusters`
Там указан devnet endpoint:
- `https://api.devnet.solana.com`
File diff suppressed because one or more lines are too long
@@ -1,114 +0,0 @@
# SHiNE Solana Arweave Viewer: как это работает
## Что лежит в папке
Основной и единственный рабочий файл viewer:
- `index.html`
Это автономный single-file viewer:
- весь HTML внутри одного файла;
- все стили внутри этого же файла;
- весь JavaScript внутри этого же файла;
- локальная копия `solana-web3` тоже встроена внутрь этого же файла.
Для запуска viewer достаточно открыть один `index.html`.
## Что делает viewer
Viewer открывается как обычная HTML-страница и работает полностью в браузере.
Он умеет:
- принимать логин пользователя SHiNE;
- принимать `Solana RPC` через форму или через URL-параметр;
- принимать `Arweave gateway` через форму или через URL-параметр;
- сохранять эти значения в `localStorage`;
- вычислять `user_pda` по логину для программы `shine_users`;
- читать `PDA` напрямую из `Solana`;
- распарсивать содержимое записи `shine_users`;
- показывать ключи, лимиты, номер записи, блокчейн пользователя и `Arweave tx id`;
- строить прямую ссылку на `Arweave`, если `tx id` есть в записи.
## Какие URL-параметры поддерживаются
Viewer понимает такие параметры:
- `login`
- `solanaRpc`
- `arweaveGateway`
Пример:
```text
index.html?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com&arweaveGateway=https%3A%2F%2Farweave.net
```
## Предустановленные адреса
### Solana RPC
- `https://solana-rpc.publicnode.com`
- `https://public.rpc.solanavibestation.com/`
### Arweave gateway
- `https://arweave.net`
## Почему одна из RPC может не работать
Адрес:
- `https://public.rpc.solanavibestation.com/`
может возвращать браузерную ошибку `403` с текстом про `Origin is not whitelisted`.
Это означает, что проблема обычно не в SHiNE viewer, а в CORS-политике самого RPC.
Для HTML-страницы, открытой прямо в браузере, это обычно не лечится со стороны клиента.
Практически для viewer лучше использовать:
- `https://solana-rpc.publicnode.com`
## Ссылки на загруженный файл в Arweave
Основная ссылка:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI`
Та же ссылка с тестовым логином `aidartest`:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest`
Та же ссылка с тестовым логином `aidartest` и явным рабочим RPC:
- `https://isvg77xrihxyzqnjex3j4uldyao7xbsfda4uhoydtigeugovjvza.arweave.net/RKpv_vFB74zBqSX2nlFjwB37hkUYOUO7A5oMShnVTXI?login=aidartest&solanaRpc=https%3A%2F%2Fsolana-rpc.publicnode.com`
## Что будет, если поставить devnet RPC
Официальный публичный `Solana devnet RPC`:
- `https://api.devnet.solana.com`
Если подставить `devnet` RPC, viewer начнёт читать именно сеть `devnet`, а не `mainnet`.
Но это не означает автоматически, что вся система SHiNE "стала тестовой".
Для реальной работы в `devnet` должны совпасть сразу несколько условий:
- программа `shine_users` должна быть задеплоена в `devnet`;
- в `devnet` должен существовать нужный `PDA` для логина;
- формат записи должен совпадать;
- данные пользователя должны реально лежать в этой тестовой сети.
Если этого нет, viewer просто не найдёт запись.
## Источник по devnet RPC
Официальная документация Solana:
- `https://solana.com/docs/references/clusters`
Там указан devnet endpoint:
- `https://api.devnet.solana.com`
+1 -1
View File
@@ -35,7 +35,7 @@ JWK используется только подготовщиком. Не по
- `prevHash` глобальной цепочки.
- `prevLineNumber/prevLineHash` канальной линии.
- ANS-104 signing/deepHash без сверки с серверной реализацией.
- `App=test5590` и `c=<canonical channel>` без согласованного изменения импортера/Viewer.
- текущий `App=<SHINE_PROTOCOL_NAMESPACE>` и `c=<canonical channel>` без согласованного изменения импортера/Viewer.
## Viewer
Viewer распознаёт Arweave URL в тексте POST и показывает изображения inline. Экран «Проверка данных» независимо считает SHA-256 Frame, проверяет доступные `prevHash` и `prevLineHash`, показывает отсутствие соседнего блока как «нет данных», а доказанное несовпадение — как ошибку. Подпись ANS-104 нельзя помечать локально проверенной, если gateway не предоставил сырую подпись DataItem.
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
import re
_UI_PROTOCOL_TAGS = Path(__file__).resolve().parents[2] / 'shine-UI' / 'js' / 'services' / 'protocol-tags.js'
_NAMESPACE_RE = re.compile(r"SHINE_PROTOCOL_NAMESPACE\s*=\s*['\"]([^'\"]+)['\"]")
def _read_namespace() -> str:
source = _UI_PROTOCOL_TAGS.read_text(encoding='utf-8')
match = _NAMESPACE_RE.search(source)
if not match:
raise RuntimeError(f'Не найден SHINE_PROTOCOL_NAMESPACE в {_UI_PROTOCOL_TAGS}')
value = match.group(1).strip()
if not value:
raise RuntimeError('SHiNE protocol namespace пуст')
return value
NAMESPACE = _read_namespace()
APP_TAG_NAME = 'App'
APP_TAG_VALUE = NAMESPACE
CHANNEL_TAG_NAME = 'c'
TYPE_TAG_NAME = 't'
+17 -4
View File
@@ -1,11 +1,24 @@
#!/usr/bin/env python3
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
from pathlib import Path
import re
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from websockets.asyncio.client import connect
ZERO32 = bytes(32)
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
def shine_protocol_namespace():
here = Path(__file__).resolve()
for parent in here.parents:
cfg = parent / 'shine-UI' / 'js' / 'services' / 'protocol-tags.js'
if cfg.exists():
m = re.search(r"SHINE_PROTOCOL_NAMESPACE\s*=\s*['\"]([^'\"]+)['\"]", cfg.read_text(encoding='utf-8'))
if m:
return m.group(1)
raise RuntimeError('Не найден shine-UI/js/services/protocol-tags.js с SHINE_PROTOCOL_NAMESPACE')
SHINE_PROTOCOL_NAMESPACE = shine_protocol_namespace()
def atomic_json(path, obj):
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
@@ -54,9 +67,9 @@ def post_body(line_code, prev_line_num, prev_line_hash, this_line_num, text):
if len(tb)>65535: raise ValueError('post text too long')
return struct.pack('>ii32siH',line_code,prev_line_num,prev_line_hash,this_line_num,len(tb))+tb
def frame(prev_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def frame(prev_hash, back32_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
return struct.pack('>H32s32siiqHHH',FRAME_CODE_V1,prev_hash,back32_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def h32(b): return hashlib.sha256(b).digest()
def hx(b): return b.hex()
@@ -139,7 +152,7 @@ async def publish_one(cfg,st,items):
else: print('Queue exhausted. Nothing to publish.'); return False
text=items[idx]['text'].strip(); block_num=int(st['last_block_number'])+1; prev_hash=bytes.fromhex(st['last_block_hash']); this_line=int(st['last_line_number'])+1
body=post_body(int(cfg['channel_root_block_number']),int(st['last_line_number']),bytes.fromhex(st['last_line_hash']),this_line,text)
fr=frame(prev_hash,block_num,body); block_hash=h32(fr); tags=[('App','test5590'),('c',cfg['channel_name'].strip().lower())]
back32_hash=ZERO32 if block_num<32 else bytes.fromhex((await block(cfg,block_num-32))[0]); fr=frame(prev_hash,back32_hash,block_num,body); block_hash=h32(fr); tags=[('App',SHINE_PROTOCOL_NAMESPACE),('c',cfg['channel_name'].strip().lower())]
raw=data_item(read_key(cfg['key_file']),tags,fr)
nextidx=idx+1
p={'queue_index':idx,'next_index_after':nextidx,'block_number':block_num,'block_hash':hx(block_hash),'previous_block_number':st['last_block_number'],'previous_block_hash':st['last_block_hash'],'this_line_number':this_line,'data_item_b64':base64.b64encode(raw).decode()}
+17 -4
View File
@@ -1,11 +1,24 @@
#!/usr/bin/env python3
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
from pathlib import Path
import re
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from websockets.asyncio.client import connect
ZERO32 = bytes(32)
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
def shine_protocol_namespace():
here = Path(__file__).resolve()
for parent in here.parents:
cfg = parent / 'shine-UI' / 'js' / 'services' / 'protocol-tags.js'
if cfg.exists():
m = re.search(r"SHINE_PROTOCOL_NAMESPACE\s*=\s*['\"]([^'\"]+)['\"]", cfg.read_text(encoding='utf-8'))
if m:
return m.group(1)
raise RuntimeError('Не найден shine-UI/js/services/protocol-tags.js с SHINE_PROTOCOL_NAMESPACE')
SHINE_PROTOCOL_NAMESPACE = shine_protocol_namespace()
def atomic_json(path, obj):
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
@@ -54,9 +67,9 @@ def post_body(line_code, prev_line_num, prev_line_hash, this_line_num, text):
if len(tb)>65535: raise ValueError('post text too long')
return struct.pack('>ii32siH',line_code,prev_line_num,prev_line_hash,this_line_num,len(tb))+tb
def frame(prev_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def frame(prev_hash, back32_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
return struct.pack('>H32s32siiqHHH',FRAME_CODE_V1,prev_hash,back32_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def h32(b): return hashlib.sha256(b).digest()
def hx(b): return b.hex()
@@ -139,7 +152,7 @@ async def publish_one(cfg,st,items):
else: print('Queue exhausted. Nothing to publish.'); return False
text=items[idx]['text'].strip(); block_num=int(st['last_block_number'])+1; prev_hash=bytes.fromhex(st['last_block_hash']); this_line=int(st['last_line_number'])+1
body=post_body(int(cfg['channel_root_block_number']),int(st['last_line_number']),bytes.fromhex(st['last_line_hash']),this_line,text)
fr=frame(prev_hash,block_num,body); block_hash=h32(fr); tags=[('App','test5590'),('c',cfg['channel_name'].strip().lower())]
back32_hash=ZERO32 if block_num<32 else bytes.fromhex((await block(cfg,block_num-32))[0]); fr=frame(prev_hash,back32_hash,block_num,body); block_hash=h32(fr); tags=[('App',SHINE_PROTOCOL_NAMESPACE),('c',cfg['channel_name'].strip().lower())]
raw=data_item(read_key(cfg['key_file']),tags,fr)
nextidx=idx+1
p={'queue_index':idx,'next_index_after':nextidx,'block_number':block_num,'block_hash':hx(block_hash),'previous_block_number':st['last_block_number'],'previous_block_hash':st['last_block_hash'],'this_line_number':this_line,'data_item_b64':base64.b64encode(raw).decode()}
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
from pathlib import Path
import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
@@ -8,6 +9,18 @@ from websockets.asyncio.client import connect
ZERO32 = bytes(32)
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
def shine_protocol_namespace():
here = Path(__file__).resolve()
for parent in here.parents:
cfg = parent / 'shine-UI' / 'js' / 'services' / 'protocol-tags.js'
if cfg.exists():
m = re.search(r"SHINE_PROTOCOL_NAMESPACE\s*=\s*['\"]([^'\"]+)['\"]", cfg.read_text(encoding='utf-8'))
if m:
return m.group(1)
raise RuntimeError('Не найден shine-UI/js/services/protocol-tags.js с SHINE_PROTOCOL_NAMESPACE')
SHINE_PROTOCOL_NAMESPACE = shine_protocol_namespace()
def atomic_json(path, obj):
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
@@ -56,9 +69,9 @@ def post_body(line_code, prev_line_num, prev_line_hash, this_line_num, text):
if len(tb)>65535: raise ValueError('post text too long')
return struct.pack('>ii32siH',line_code,prev_line_num,prev_line_hash,this_line_num,len(tb))+tb
def frame(prev_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def frame(prev_hash, back32_hash, block_num, body, ts=None):
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
return struct.pack('>H32s32siiqHHH',FRAME_CODE_V1,prev_hash,back32_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+body
def h32(b): return hashlib.sha256(b).digest()
def hx(b): return b.hex()
@@ -141,7 +154,7 @@ async def publish_one(cfg,st,items):
else: print('Queue exhausted. Nothing to publish.'); return False
text=items[idx]['text'].strip(); block_num=int(st['last_block_number'])+1; prev_hash=bytes.fromhex(st['last_block_hash']); this_line=int(st['last_line_number'])+1
body=post_body(int(cfg['channel_root_block_number']),int(st['last_line_number']),bytes.fromhex(st['last_line_hash']),this_line,text)
fr=frame(prev_hash,block_num,body); block_hash=h32(fr); tags=[('App','test5590'),('c',cfg['channel_name'].strip().lower())]
back32_hash=ZERO32 if block_num<32 else bytes.fromhex((await block(cfg,block_num-32))[0]); fr=frame(prev_hash,back32_hash,block_num,body); block_hash=h32(fr); tags=[('App',SHINE_PROTOCOL_NAMESPACE),('c',cfg['channel_name'].strip().lower())]
raw=data_item(read_key(cfg['key_file']),tags,fr)
nextidx=idx+1
p={'queue_index':idx,'next_index_after':nextidx,'block_number':block_num,'block_hash':hx(block_hash),'previous_block_number':st['last_block_number'],'previous_block_hash':st['last_block_hash'],'this_line_number':this_line,'data_item_b64':base64.b64encode(raw).decode()}
@@ -236,7 +249,7 @@ async def publish_channel(cfg,st,ch,force=False):
prev_line_block=int(cs.get('last_line_block_number', ch['channel_root_block_number']))
body=post_body(int(ch['channel_root_block_number']),prev_line_block,bytes.fromhex(cs['last_line_hash']),this_line,text)
fr=frame(prev_hash,block_num,body); block_hash=h32(fr)
tags=[('App','test5590'),('c',ch['channel_name'].strip().lower())]
tags=[('App',SHINE_PROTOCOL_NAMESPACE),('c',ch['channel_name'].strip().lower())]
raw=data_item(read_key(cfg['key_file']),tags,fr)
p={'channel_name':ch['channel_name'],'queue_index':idx,'next_index_after':idx+1,'block_number':block_num,
'block_hash':hx(block_hash),'previous_block_number':st['last_block_number'],'previous_block_hash':st['last_block_hash'],