SHA256
back32hash 5591: готово к тестам
Вроде как всё готово к тестам.
This commit is contained in:
@@ -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
|
|
||||||
+1
-1
@@ -26,7 +26,7 @@ import java.time.Duration;
|
|||||||
import java.util.*;
|
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
|
* The importer is transport-agnostic: a DataItem may have reached Arweave through Turbo
|
||||||
* or inside a direct server-created ANS-104 bundle.
|
* or inside a direct server-created ANS-104 bundle.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
package server.archive;
|
package server.archive;
|
||||||
|
|
||||||
import blockchain.Ans104DataItem;
|
import blockchain.Ans104DataItem;
|
||||||
|
import blockchain.ShineProtocolTags;
|
||||||
import utils.config.AppConfig;
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
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_NAME = ShineProtocolTags.APP_TAG_NAME;
|
||||||
public static final String TEST_TAG_VALUE = "test5590";
|
public static final String TEST_TAG_VALUE = ShineProtocolTags.APP_TAG_VALUE;
|
||||||
public static final String CHANNEL_TAG_NAME = "c_test5590";
|
public static final String CHANNEL_TAG_NAME = ShineProtocolTags.CHANNEL_TAG_NAME;
|
||||||
|
|
||||||
public static ArweaveBlocksConfig load() {
|
public static ArweaveBlocksConfig load() {
|
||||||
AppConfig c = AppConfig.getInstance();
|
AppConfig c = AppConfig.getInstance();
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ final class Ans104BundleTest {
|
|||||||
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("bundle-" + payload);
|
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("bundle-" + payload);
|
||||||
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
||||||
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
|
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);
|
byte[] signature = Ed25519Util.sign(Ans104DataItem.buildSigningMessage(owner, tags, data), privateKey);
|
||||||
return Ans104DataItem.serializeEd25519(signature, owner, tags, data);
|
return Ans104DataItem.serializeEd25519(signature, owner, tags, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import java.util.Objects;
|
|||||||
* SHiNE Frame v1 (all integer fields BigEndian):
|
* SHiNE Frame v1 (all integer fields BigEndian):
|
||||||
* [2] frameCode = 0x0001
|
* [2] frameCode = 0x0001
|
||||||
* [32] prevHash32 = SHA-256(Frame v1 bytes of previous SHiNE block)
|
* [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] blockSize = exact Frame v1 byte length
|
||||||
* [4] blockNumber
|
* [4] blockNumber
|
||||||
* [8] timestamp unix seconds
|
* [8] timestamp unix seconds
|
||||||
@@ -33,10 +34,11 @@ public final class BchBlockEntry {
|
|||||||
public static final int FRAME_CODE_V1 = 0x0001;
|
public static final int FRAME_CODE_V1 = 0x0001;
|
||||||
public static final int MAX_BLOCK_FULL_BYTES = Ans104DataItem.MAX_DATA_ITEM_BYTES;
|
public static final int MAX_BLOCK_FULL_BYTES = Ans104DataItem.MAX_DATA_ITEM_BYTES;
|
||||||
public static final long MAX_FUTURE_SECONDS = 60;
|
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 int frameCode;
|
||||||
public final byte[] prevHash32;
|
public final byte[] prevHash32;
|
||||||
|
public final byte[] back32Hash32;
|
||||||
public final int blockSize;
|
public final int blockSize;
|
||||||
public final int blockNumber;
|
public final int blockNumber;
|
||||||
public final long timestamp;
|
public final long timestamp;
|
||||||
@@ -66,6 +68,8 @@ public final class BchBlockEntry {
|
|||||||
}
|
}
|
||||||
this.prevHash32 = new byte[32];
|
this.prevHash32 = new byte[32];
|
||||||
bb.get(prevHash32);
|
bb.get(prevHash32);
|
||||||
|
this.back32Hash32 = new byte[32];
|
||||||
|
bb.get(back32Hash32);
|
||||||
this.blockSize = bb.getInt();
|
this.blockSize = bb.getInt();
|
||||||
if (blockSize != frameBytes.length || blockSize < FRAME_HEADER_SIZE) {
|
if (blockSize != frameBytes.length || blockSize < FRAME_HEADER_SIZE) {
|
||||||
throw new IllegalArgumentException("SHiNE blockSize mismatch: declared=" + blockSize + " actual=" + frameBytes.length);
|
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";
|
||||||
|
}
|
||||||
+7
-7
@@ -15,8 +15,8 @@ final class Ans104DataItemTest {
|
|||||||
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
||||||
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
|
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
|
||||||
List<Ans104DataItem.Tag> tags = List.of(
|
List<Ans104DataItem.Tag> tags = List.of(
|
||||||
new Ans104DataItem.Tag("App", "test5590"),
|
new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE),
|
||||||
new Ans104DataItem.Tag("c_test5590", "books")
|
new Ans104DataItem.Tag(ShineProtocolTags.CHANNEL_TAG_NAME, "books")
|
||||||
);
|
);
|
||||||
|
|
||||||
byte[] message = Ans104DataItem.buildSigningMessage(owner, tags, data);
|
byte[] message = Ans104DataItem.buildSigningMessage(owner, tags, data);
|
||||||
@@ -27,8 +27,8 @@ final class Ans104DataItemTest {
|
|||||||
assertEquals(2, parsed.signatureType());
|
assertEquals(2, parsed.signatureType());
|
||||||
assertArrayEquals(owner, parsed.owner32());
|
assertArrayEquals(owner, parsed.owner32());
|
||||||
assertArrayEquals(data, parsed.data());
|
assertArrayEquals(data, parsed.data());
|
||||||
assertTrue(parsed.hasTag("App", "test5590"));
|
assertTrue(parsed.hasTag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE));
|
||||||
assertEquals("books", parsed.tagValue("c_test5590"));
|
assertEquals("books", parsed.tagValue(ShineProtocolTags.CHANNEL_TAG_NAME));
|
||||||
assertTrue(parsed.verifySignature());
|
assertTrue(parsed.verifySignature());
|
||||||
assertEquals(32, parsed.id32().length);
|
assertEquals(32, parsed.id32().length);
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ final class Ans104DataItemTest {
|
|||||||
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("shine-ans104-mutation-key");
|
byte[] privateKey = Ed25519Util.generatePrivateKeyFromString("shine-ans104-mutation-key");
|
||||||
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
byte[] owner = Ed25519Util.derivePublicKey(privateKey);
|
||||||
byte[] data = "original".getBytes(StandardCharsets.UTF_8);
|
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[] signature = Ed25519Util.sign(Ans104DataItem.buildSigningMessage(owner, tags, data), privateKey);
|
||||||
byte[] raw = Ans104DataItem.serializeEd25519(signature, owner, tags, data);
|
byte[] raw = Ans104DataItem.serializeEd25519(signature, owner, tags, data);
|
||||||
raw[raw.length - 1] ^= 1;
|
raw[raw.length - 1] ^= 1;
|
||||||
@@ -50,8 +50,8 @@ final class Ans104DataItemTest {
|
|||||||
for (int i = 0; i < owner.length; i++) owner[i] = (byte) i;
|
for (int i = 0; i < owner.length; i++) owner[i] = (byte) i;
|
||||||
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
|
byte[] data = "frame-v1-test".getBytes(StandardCharsets.UTF_8);
|
||||||
List<Ans104DataItem.Tag> tags = List.of(
|
List<Ans104DataItem.Tag> tags = List.of(
|
||||||
new Ans104DataItem.Tag("App", "test5590"),
|
new Ans104DataItem.Tag(ShineProtocolTags.APP_TAG_NAME, ShineProtocolTags.APP_TAG_VALUE),
|
||||||
new Ans104DataItem.Tag("c_test5590", "books")
|
new Ans104DataItem.Tag(ShineProtocolTags.CHANNEL_TAG_NAME, "books")
|
||||||
);
|
);
|
||||||
|
|
||||||
byte[] actual = Ans104DataItem.buildSigningMessage(owner, tags, data);
|
byte[] actual = Ans104DataItem.buildSigningMessage(owner, tags, data);
|
||||||
|
|||||||
+30
-4
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.blockchain;
|
|||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
|
import blockchain.ShineProtocolTags;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
import blockchain.body.ConnectionBody;
|
import blockchain.body.ConnectionBody;
|
||||||
@@ -203,6 +204,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
case "bad_block_number" -> "Некорректный номер блока";
|
case "bad_block_number" -> "Некорректный номер блока";
|
||||||
case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
|
case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
|
||||||
case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
|
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 "bad_blockchain_key_len" -> "Некорректный ключ блокчейна в состоянии (ожидалось 32 байта)";
|
||||||
case "signature_verify_failed" -> "Ошибка проверки подписи блока";
|
case "signature_verify_failed" -> "Ошибка проверки подписи блока";
|
||||||
case "bad_signature" -> "Некорректная подпись блока";
|
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);
|
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);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_app_tag", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signed secondary index for the fixed SHiNE streams.
|
// Signed secondary index for the fixed SHiNE streams.
|
||||||
String expectedTypeTag = expectedTypeTag(block);
|
String expectedTypeTag = expectedTypeTag(block);
|
||||||
String actualTypeTag = block.getDataItem().tagValue("t");
|
String actualTypeTag = block.getDataItem().tagValue(ShineProtocolTags.TYPE_TAG_NAME);
|
||||||
if (expectedTypeTag != null) {
|
if (expectedTypeTag != null) {
|
||||||
if (!expectedTypeTag.equals(actualTypeTag)) {
|
if (!expectedTypeTag.equals(actualTypeTag)) {
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_type_tag", serverLastNum, serverLastHashHex);
|
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);
|
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 {
|
try {
|
||||||
String expectedChannelSlug = expectedChannelSlug(blockchainName, block, channelNameStateEntry);
|
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 != null) {
|
||||||
if (!expectedChannelSlug.equals(actualChannelSlug)) {
|
if (!expectedChannelSlug.equals(actualChannelSlug)) {
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_tag", serverLastNum, serverLastHashHex);
|
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);
|
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
|
// 5) pubKey
|
||||||
final byte[] pubKey32 = st.getBlockchainKeyBytes();
|
final byte[] pubKey32 = st.getBlockchainKeyBytes();
|
||||||
if (pubKey32 == null || pubKey32.length != 32) {
|
if (pubKey32 == null || pubKey32.length != 32) {
|
||||||
|
|||||||
+15
-3
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.keyRotation;
|
|||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
|
import blockchain.ShineProtocolTags;
|
||||||
import blockchain.body.ForkBody;
|
import blockchain.body.ForkBody;
|
||||||
import server.logic.ws_protocol.Base64Ws;
|
import server.logic.ws_protocol.Base64Ws;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
@@ -98,17 +99,28 @@ public final class Net_KeyRotationAddBlock_Handler implements JsonMessageHandler
|
|||||||
if (block.blockNumber != req.getBlockNumber()) {
|
if (block.blockNumber != req.getBlockNumber()) {
|
||||||
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BLOCK_NUMBER_MISMATCH", "blockNumber запроса не совпадает с блоком");
|
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BLOCK_NUMBER_MISMATCH", "blockNumber запроса не совпадает с блоком");
|
||||||
}
|
}
|
||||||
if (!block.getDataItem().hasTag("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=test5590");
|
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_APP_TAG", "Отсутствует App=" + ShineProtocolTags.APP_TAG_VALUE);
|
||||||
}
|
}
|
||||||
String expectedTypeTag = expectedTypeTag(block);
|
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) {
|
if (expectedTypeTag != null ? !expectedTypeTag.equals(actualTypeTag) : actualTypeTag != null) {
|
||||||
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_TYPE_TAG", "Некорректный t-тег candidate DataItem");
|
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_TYPE_TAG", "Некорректный t-тег candidate DataItem");
|
||||||
}
|
}
|
||||||
if (!requestPrevHashMatches(req.getPrevBlockHash(), block.prevHash32)) {
|
if (!requestPrevHashMatches(req.getPrevBlockHash(), block.prevHash32)) {
|
||||||
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_PREV_HASH_MISMATCH", "prevBlockHash запроса не совпадает с блоком");
|
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;
|
byte[] newBlockchainKey;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+11
-2
@@ -3,6 +3,7 @@ package server.logic.ws_protocol.JSON.handlers.keyRotation;
|
|||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
|
import blockchain.ShineProtocolTags;
|
||||||
import blockchain.body.ForkBody;
|
import blockchain.body.ForkBody;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
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 (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.getHash32(), stored.getBlockHash())) return "KEY_ROTATION_STORED_HASH_MISMATCH";
|
||||||
if (!Arrays.equals(candidate.getDataItemId32(), stored.getDataItemId())) return "KEY_ROTATION_STORED_DATAITEM_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 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 (expectedTypeTag != null ? !expectedTypeTag.equals(actualTypeTag) : actualTypeTag != null) return "KEY_ROTATION_BAD_TYPE_TAG";
|
||||||
if (!BchCryptoVerifier.verifyBlock(candidate, newBlockchainKey)) return "KEY_ROTATION_BAD_SIGNATURE";
|
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";
|
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);
|
String semanticError = validateAgainstRotation(c, session, candidate);
|
||||||
if (semanticError != null) return semanticError;
|
if (semanticError != null) return semanticError;
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ test.freeAvatar.walletJwkPath=
|
|||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Arweave per-user-block transport (ANS-104)
|
# 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>.
|
# Channel DataItems additionally contain c=<canonical-channel-slug>.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
arweave.blocks.publish.enabled=false
|
arweave.blocks.publish.enabled=false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package test.it.blockchain;
|
|||||||
|
|
||||||
import blockchain.Ans104DataItem;
|
import blockchain.Ans104DataItem;
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
|
import blockchain.ShineProtocolTags;
|
||||||
import blockchain.body.*;
|
import blockchain.body.*;
|
||||||
import test.it.utils.TestConfig;
|
import test.it.utils.TestConfig;
|
||||||
import test.it.utils.TestIds;
|
import test.it.utils.TestIds;
|
||||||
@@ -68,6 +69,7 @@ public final class AddBlockSender {
|
|||||||
|
|
||||||
int blockNumber = state.nextBlockNumber();
|
int blockNumber = state.nextBlockNumber();
|
||||||
byte[] prevHash32 = state.prevHash32ForNext();
|
byte[] prevHash32 = state.prevHash32ForNext();
|
||||||
|
byte[] back32Hash32 = state.back32Hash32ForNext();
|
||||||
long tsSec = System.currentTimeMillis() / 1000L;
|
long tsSec = System.currentTimeMillis() / 1000L;
|
||||||
|
|
||||||
short type = typeOf(body);
|
short type = typeOf(body);
|
||||||
@@ -76,15 +78,15 @@ public final class AddBlockSender {
|
|||||||
|
|
||||||
byte[] bodyBytes = body.toBytes();
|
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);
|
byte[] owner32 = utils.crypto.Ed25519Util.derivePublicKey(loginPrivKey);
|
||||||
|
|
||||||
List<Ans104DataItem.Tag> tags = new ArrayList<>();
|
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);
|
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);
|
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[] signingMessage = Ans104DataItem.buildSigningMessage(owner32, tags, frame);
|
||||||
byte[] signature64 = utils.crypto.Ed25519Util.sign(signingMessage, loginPrivKey);
|
byte[] signature64 = utils.crypto.Ed25519Util.sign(signingMessage, loginPrivKey);
|
||||||
@@ -232,6 +234,7 @@ public final class AddBlockSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] buildFrame(byte[] prevHash32,
|
private static byte[] buildFrame(byte[] prevHash32,
|
||||||
|
byte[] back32Hash32,
|
||||||
int blockNumber,
|
int blockNumber,
|
||||||
long tsSec,
|
long tsSec,
|
||||||
short type,
|
short type,
|
||||||
@@ -242,12 +245,16 @@ public final class AddBlockSender {
|
|||||||
if (prevHash32 == null || prevHash32.length != 32) {
|
if (prevHash32 == null || prevHash32.length != 32) {
|
||||||
throw new IllegalArgumentException("prevHash32 must be 32 bytes");
|
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 bodyLen = bodyBytes == null ? 0 : bodyBytes.length;
|
||||||
int blockSize = BchBlockEntry.FRAME_HEADER_SIZE + bodyLen;
|
int blockSize = BchBlockEntry.FRAME_HEADER_SIZE + bodyLen;
|
||||||
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
|
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||||
bb.putShort((short) BchBlockEntry.FRAME_CODE_V1);
|
bb.putShort((short) BchBlockEntry.FRAME_CODE_V1);
|
||||||
bb.put(prevHash32);
|
bb.put(prevHash32);
|
||||||
|
bb.put(back32Hash32);
|
||||||
bb.putInt(blockSize);
|
bb.putInt(blockSize);
|
||||||
bb.putInt(blockNumber);
|
bb.putInt(blockNumber);
|
||||||
bb.putLong(tsSec);
|
bb.putLong(tsSec);
|
||||||
|
|||||||
@@ -123,10 +123,18 @@ public final class ChainState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] prevHash32ForNext() {
|
public byte[] prevHash32ForNext() {
|
||||||
if (lastBlockNumber < 0) return ZERO32;
|
if (lastBlockNumber < 0) return ZERO32.clone();
|
||||||
return hexToBytes32(lastBlockHashHex);
|
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() {
|
public byte[] headerHash32() {
|
||||||
return headerHash32 == null ? null : headerHash32.clone();
|
return headerHash32 == null ? null : headerHash32.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.14.5
|
client.version=1.14.6
|
||||||
server.version=1.12.3
|
server.version=1.12.4
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
- `blockchainName` — целевая пользовательская chain;
|
- `blockchainName` — целевая пользовательская chain;
|
||||||
- `blockNumber` — должен совпадать с номером внутри Frame v1 и быть `serverLast + 1`;
|
- `blockNumber` — должен совпадать с номером внутри Frame v1 и быть `serverLast + 1`;
|
||||||
- `prevBlockHash` — 32-byte SHiNE hash предыдущего Frame в hex; должен совпадать с `prevHash32` внутри Frame и серверной вершиной;
|
- `prevBlockHash` — 32-byte SHiNE hash предыдущего Frame в hex; должен совпадать с `prevHash32` внутри Frame и серверной вершиной;
|
||||||
|
- внутри Frame также обязателен `back32Hash32`: нули для blockNumber < 32, иначе hash Frame блока `blockNumber-32`;
|
||||||
- `blockBytesB64` — **полный serialized ANS-104 DataItem** в Base64.
|
- `blockBytesB64` — **полный serialized ANS-104 DataItem** в Base64.
|
||||||
|
|
||||||
Старый формат `preimage + sigMarker + signature` не поддерживается.
|
Старый формат `preimage + sigMarker + signature` не поддерживается.
|
||||||
@@ -31,13 +32,13 @@
|
|||||||
- generic Ed25519 signature type `2`;
|
- generic Ed25519 signature type `2`;
|
||||||
- Ed25519 signature 64 bytes;
|
- Ed25519 signature 64 bytes;
|
||||||
- owner 32 bytes и равен текущему blockchain public key;
|
- owner 32 bytes и равен текущему blockchain public key;
|
||||||
- обязательный тестовый tag `App=test5590`;
|
- обязательный тестовый tag `App=test5591`;
|
||||||
- для channel block — `c=<canonical_channel_slug>`;
|
- для channel block — `c=<canonical_channel_slug>`;
|
||||||
- `data` содержит SHiNE Frame v1 (`frameCode=1`).
|
- `data` содержит SHiNE Frame v1 (`frameCode=1`).
|
||||||
|
|
||||||
## Основные ошибки
|
## Основные ошибки
|
||||||
|
|
||||||
- `bad_app_tag` — отсутствует/неверен `App=test5590`;
|
- `bad_app_tag` — отсутствует/неверен `App=test5591`;
|
||||||
- `bad_channel_tag` — `c` отсутствует, лишний или не совпадает с canonical slug;
|
- `bad_channel_tag` — `c` отсутствует, лишний или не совпадает с canonical slug;
|
||||||
- `bad_signature` / `signature_verify_failed` — DataItem не подписан текущим blockchain key;
|
- `bad_signature` / `signature_verify_failed` — DataItem не подписан текущим blockchain key;
|
||||||
- `bad_block_number` — нарушена последовательность;
|
- `bad_block_number` — нарушена последовательность;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ ANS-104 DataItem
|
|||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| `frameCode` | 2 | `0x0001` |
|
| `frameCode` | 2 | `0x0001` |
|
||||||
| `prevHash32` | 32 | SHA-256 полного Frame v1 предыдущего SHiNE-блока; для блока 0 — нули |
|
| `prevHash32` | 32 | SHA-256 полного Frame v1 предыдущего SHiNE-блока; для блока 0 — нули |
|
||||||
|
| `back32Hash32` | 32 | SHA-256 Frame блока `N-32`; для `N<32` — 32 нулевых байта |
|
||||||
| `blockSize` | 4 | точный размер Frame v1, включая header и body |
|
| `blockSize` | 4 | точный размер Frame v1, включая header и body |
|
||||||
| `blockNumber` | 4 | номер блока, начиная с 0 |
|
| `blockNumber` | 4 | номер блока, начиная с 0 |
|
||||||
| `timestamp` | 8 | Unix time seconds |
|
| `timestamp` | 8 | Unix time seconds |
|
||||||
@@ -35,11 +36,12 @@ ANS-104 DataItem
|
|||||||
| `version` | 2 | версия body |
|
| `version` | 2 | версия body |
|
||||||
| `body` | N | данные конкретного типа |
|
| `body` | N | данные конкретного типа |
|
||||||
|
|
||||||
`FRAME_HEADER_SIZE = 56` bytes.
|
`FRAME_HEADER_SIZE = 88` bytes.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
blockHash32 = SHA256(FrameV1Bytes)
|
blockHash32 = SHA256(FrameV1Bytes)
|
||||||
next.prevHash32 = blockHash32
|
next.prevHash32 = blockHash32
|
||||||
|
blockN.back32Hash32 = (N < 32 ? ZERO_HASH_32 : HASH(block N-32))
|
||||||
```
|
```
|
||||||
|
|
||||||
Подписи внутри Frame v1 нет. Единственная подпись пользователя — подпись окружающего ANS-104 DataItem.
|
Подписи внутри Frame v1 нет. Единственная подпись пользователя — подпись окружающего ANS-104 DataItem.
|
||||||
@@ -72,7 +74,7 @@ next.prevHash32 = blockHash32
|
|||||||
Каждый блок:
|
Каждый блок:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
App = test5590
|
App = test5591
|
||||||
```
|
```
|
||||||
|
|
||||||
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
|
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
|
||||||
@@ -94,7 +96,7 @@ t=prof # USER_PROFILE
|
|||||||
Если блок относится к конкретному каналу, он дополнительно содержит:
|
Если блок относится к конкретному каналу, он дополнительно содержит:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
c_test5590 = <canonical_channel_slug>
|
c = <canonical_channel_slug>
|
||||||
```
|
```
|
||||||
|
|
||||||
Slug входит в подпись DataItem и не может быть изменён сервером после подписи.
|
Slug входит в подпись DataItem и не может быть изменён сервером после подписи.
|
||||||
@@ -118,16 +120,17 @@ Slug входит в подпись DataItem и не может быть изм
|
|||||||
Сервер обязан:
|
Сервер обязан:
|
||||||
|
|
||||||
1. распарсить полный ANS-104 DataItem;
|
1. распарсить полный ANS-104 DataItem;
|
||||||
2. проверить `App=test5590`;
|
2. проверить `App=test5591`;
|
||||||
3. проверить `t`, если тип блока требует фиксированный индексный поток;
|
3. проверить `t`, если тип блока требует фиксированный индексный поток;
|
||||||
4. проверить `c_test5590`, если тип блока требует канал;
|
4. проверить `c`, если тип блока требует канал;
|
||||||
5. проверить ANS-104 Ed25519 подпись;
|
5. проверить ANS-104 Ed25519 подпись;
|
||||||
6. проверить, что `owner` равен текущему blockchain public key пользователя;
|
6. проверить, что `owner` равен текущему blockchain public key пользователя;
|
||||||
7. распарсить Frame v1 и body;
|
7. распарсить Frame v1 и body;
|
||||||
8. проверить `blockNumber == last + 1`;
|
8. проверить `blockNumber == last + 1`;
|
||||||
9. проверить `prevHash32 == lastBlockHash`;
|
9. проверить `prevHash32 == lastBlockHash`;
|
||||||
10. проверить строгую непрерывность логической линии, если body line-based;
|
10. проверить `back32Hash32`: нули для `N<32`, иначе hash блока `N-32`;
|
||||||
11. записать DataItem и новое состояние атомарно в PostgreSQL.
|
11. проверить строгую непрерывность логической линии, если body line-based;
|
||||||
|
12. записать DataItem и новое состояние атомарно в PostgreSQL.
|
||||||
|
|
||||||
|
|
||||||
## Нормативное правило версий body
|
## Нормативное правило версий body
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 2. Логические линии внутри пользовательской цепочки
|
## 2. Логические линии внутри пользовательской цепочки
|
||||||
|
|
||||||
Глобально все блоки пользователя идут одной последовательностью `blockNumber` и `prevHash32`. Дополнительно некоторые типы образуют собственные проверяемые линии.
|
Глобально все блоки пользователя идут одной последовательностью `blockNumber` и `prevHash32`; каждый Frame также содержит резервный `back32Hash32` на блок `N-32` (нули для `N<32`). Дополнительно некоторые типы образуют собственные проверяемые линии. Отдельного back32 внутри линий нет.
|
||||||
|
|
||||||
Актуальные линии:
|
Актуальные линии:
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ HEADER сам не хранит line-prefix, но является root TECH-л
|
|||||||
- является очередным шагом TECH-линии;
|
- является очередным шагом TECH-линии;
|
||||||
- является root нового канала;
|
- является root нового канала;
|
||||||
- имеет `t=tech`;
|
- имеет `t=tech`;
|
||||||
- имеет `c_test5590=<canonical_channel_slug>`.
|
- имеет `c=<canonical_channel_slug>`.
|
||||||
|
|
||||||
Начало body:
|
Начало body:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# ANS-104 / Arweave transport для пользовательских блоков SHiNE
|
# 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.
|
Каждый пользовательский блок SHiNE уже на клиенте является самостоятельным подписанным ANS-104 DataItem. Сервер проверяет и хранит **точно эти signed bytes** и может публиковать их одним из двух транспортов: через Turbo по одному DataItem либо через прямую Arweave L1-транзакцию в составе стандартного большого ANS-104 bundle.
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
Для тестового контура обязательно:
|
Для тестового контура обязательно:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
App=test5590
|
App=test5591
|
||||||
```
|
```
|
||||||
|
|
||||||
Для фиксированных потоков:
|
Для фиксированных потоков:
|
||||||
@@ -25,10 +27,10 @@ t=prof # USER_PROFILE
|
|||||||
Для блоков конкретного канала дополнительно:
|
Для блоков конкретного канала дополнительно:
|
||||||
|
|
||||||
```text
|
```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.
|
Теги входят в ANS-104 подпись пользователя. Сервер проверяет соответствие `t` фактическому `msg_type` и отклоняет лишний/неверный `t`. Старый тестовый тег `c` новым кодом не создаётся и не принимается как channel tag.
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ Bundle-Version=2.0.0
|
|||||||
Content-Type=application/octet-stream
|
Content-Type=application/octet-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
Специальный `App=test5590-batch` больше не используется. Важны вложенные user DataItems, у которых уже есть `App=test5590`.
|
Специальный `App=test5591-batch` больше не используется. Важны вложенные user DataItems, у которых уже есть `App=test5591`.
|
||||||
|
|
||||||
### `none`
|
### `none`
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ Content-Type=application/octet-stream
|
|||||||
Importer всегда выполняет один discovery-запрос:
|
Importer всегда выполняет один discovery-запрос:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
App=test5590
|
App=test5591
|
||||||
```
|
```
|
||||||
|
|
||||||
Он **не ищет root bundles** и не зависит от `publish.mode`.
|
Он **не ищет root bundles** и не зависит от `publish.mode`.
|
||||||
@@ -130,7 +132,7 @@ App=test5590
|
|||||||
|
|
||||||
Поэтому importer:
|
Поэтому importer:
|
||||||
|
|
||||||
1. получает `data_item_id` через GraphQL `App=test5590`;
|
1. получает `data_item_id` через GraphQL `App=test5591`;
|
||||||
2. запрашивает `GET /ar-io/offsets/{data_item_id}`;
|
2. запрашивает `GET /ar-io/offsets/{data_item_id}`;
|
||||||
3. получает `rootTxId`, `rootOffset`, `size`;
|
3. получает `rootTxId`, `rootOffset`, `size`;
|
||||||
4. делает range-read `GET /raw/{rootTxId}` ровно по этому диапазону;
|
4. делает range-read `GET /raw/{rootTxId}` ровно по этому диапазону;
|
||||||
|
|||||||
@@ -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
|
## 2026-10-01 — Проверяемые USER_PROFILE / CONNECTION / TECH линии и signed `t` index
|
||||||
|
|
||||||
- Тип `4` в документации называется `USER_PROFILE`; внутренние исторические имена классов могут сохраняться. USER_PROFILE предназначен только для данных профиля.
|
- Тип `4` в документации называется `USER_PROFILE`; внутренние исторические имена классов могут сохраняться. USER_PROFILE предназначен только для данных профиля.
|
||||||
@@ -5,8 +15,8 @@
|
|||||||
- `TECH_FORK` включён в общую TECH-линию вместе с `TECH_CREATE_CHANNEL`; оба используют `lineCode=0`, предыдущий TECH block/hash и `techSequence`.
|
- `TECH_FORK` включён в общую TECH-линию вместе с `TECH_CREATE_CHANNEL`; оба используют `lineCode=0`, предыдущий TECH block/hash и `techSequence`.
|
||||||
- Серверная line-валидация стала строгой: новый блок обязан продолжать фактический текущий хвост той же линии и иметь sequence `+1`; произвольная ссылка на существующий блок больше не принимается. Это также устраняет боковую ветку канала после `EDIT_POST`.
|
- Серверная line-валидация стала строгой: новый блок обязан продолжать фактический текущий хвост той же линии и иметь sequence `+1`; произвольная ссылка на существующий блок больше не принимается. Это также устраняет боковую ветку канала после `EDIT_POST`.
|
||||||
- Добавлены подписанные ANS-104 индексы: `t=prof`, `t=conn`, `t=tech`. `STATUS_ACTION` не изменён.
|
- Добавлены подписанные ANS-104 индексы: `t=prof`, `t=conn`, `t=tech`. `STATUS_ACTION` не изменён.
|
||||||
- В отдельном `shine-solana-arweave-viewer-v3` исправлен legacy-фильтр канала `c` → канонический `c_test5590`.
|
- Единственным актуальным Viewer считается `shine-UI/static-sites/arweave-viewer`; старые корневые Viewer перенесены в его `history/legacy-root-viewers/`.
|
||||||
- `back32Hash` и дополнительные skip-ссылки не добавлялись.
|
- В общий Frame добавлен глобальный `back32Hash32`: для блоков `#0..#31` нули, начиная с `#32` — SHA-256 Frame блока `N-32`. Отдельный back32 для line-body не добавляется.
|
||||||
- Формат USER_PROFILE/CONNECTION/TECH_FORK несовместим с предыдущей тестовой схемой; проект находится до production и миграция старых блоков не предусмотрена.
|
- Формат USER_PROFILE/CONNECTION/TECH_FORK несовместим с предыдущей тестовой схемой; проект находится до production и миграция старых блоков не предусмотрена.
|
||||||
|
|
||||||
## 2026-09-27 — TECH_FORK v1
|
## 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`
|
# Применение patch: Turbo + direct Arweave для `App=test5590`
|
||||||
|
|
||||||
## Что меняется
|
## Что меняется
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
```text
|
```text
|
||||||
User
|
User
|
||||||
-> создаёт Frame v1
|
-> создаёт 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
|
-> Ed25519 подписывает ANS-104 deep-hash
|
||||||
-> готовый DataItem
|
-> готовый DataItem
|
||||||
-> AddBlock
|
-> AddBlock
|
||||||
@@ -27,7 +27,7 @@ Server
|
|||||||
ИЛИ publish.mode=none: наружу не публиковать
|
ИЛИ publish.mode=none: наружу не публиковать
|
||||||
|
|
||||||
Other servers
|
Other servers
|
||||||
-> GraphQL App=test5590
|
-> GraphQL App=test5591
|
||||||
-> скачивают/извлекают DataItem
|
-> скачивают/извлекают DataItem
|
||||||
-> verify
|
-> verify
|
||||||
-> PostgreSQL без повторной публикации
|
-> PostgreSQL без повторной публикации
|
||||||
@@ -38,4 +38,4 @@ Other servers
|
|||||||
- SHiNE `prevHash` остаётся SHA-256 предыдущего Frame v1 и не зависит от Arweave.
|
- SHiNE `prevHash` остаётся SHA-256 предыдущего Frame v1 и не зависит от Arweave.
|
||||||
- `data_item_id` нужен для Arweave, поиска и дедупликации.
|
- `data_item_id` нужен для Arweave, поиска и дедупликации.
|
||||||
- PostgreSQL — единственное локальное хранилище пользовательских блоков.
|
- PostgreSQL — единственное локальное хранилище пользовательских блоков.
|
||||||
- Тестовый namespace `App=test5590` специально отделён от будущего production namespace.
|
- Тестовый namespace `App=test5591` специально отделён от будущего production namespace.
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
# TODO — резервные ссылки `back32Hash` для SHiNE blockchain
|
# `back32Hash32` — глобальная резервная ссылка SHiNE
|
||||||
|
|
||||||
Статус: **TODO / не реализовано**.
|
Статус: **реализовано для общей пользовательской цепочки**. Отдельный `back32` для логических линий намеренно не добавляется.
|
||||||
|
|
||||||
Цель этой идеи — повысить устойчивость проверки SHiNE blockchain и отдельных логических линий к ситуации, когда один или несколько DataItem временно или постоянно недоступны у конкретного Arweave gateway, CDN, сервера или другого источника.
|
## Правило
|
||||||
|
|
||||||
Это **не замена** обычным ссылкам на предыдущий блок. Основная цепочка по-прежнему строится через `prevHash32` / `prevLineHash32`. `back32...` — только дополнительная резервная ссылка назад.
|
Каждый SHiNE Frame содержит:
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Глобальная цепочка пользователя
|
|
||||||
|
|
||||||
В общий Frame в будущем добавить одно поле:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
[32] prevHash32
|
||||||
[32] back32Hash32
|
[32] back32Hash32
|
||||||
```
|
```
|
||||||
|
|
||||||
Смысл:
|
`prevHash32` остаётся основной соседней связью `N -> N-1`.
|
||||||
|
|
||||||
|
`back32Hash32` — дополнительная резервная связь:
|
||||||
|
|
||||||
```text
|
```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)
|
back32Hash32 -> HASH(#68)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Для первых блоков
|
Для первых 32 блоков:
|
||||||
|
|
||||||
Если блока `N-32` ещё не существует, поле содержит 32 нулевых байта:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
#0 ... #31 -> back32Hash32 = ZERO_HASH_32
|
#0 ... #31 -> back32Hash32 = ZERO_HASH_32
|
||||||
#32 -> HASH(#0)
|
#32 -> HASH(#0)
|
||||||
#33 -> HASH(#1)
|
#33 -> HASH(#1)
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Номер блока отдельно НЕ хранить
|
Сервер при `AddBlock` обязан сам проверить это поле по сохранённому блоку `N-32`; клиентское значение не принимается на доверии.
|
||||||
|
|
||||||
Поле вида `back32BlockNumber` не нужно.
|
## Почему без `back32` внутри линий
|
||||||
|
|
||||||
Номер однозначно вычисляется:
|
CHANNEL / TECH / CONNECTION / USER_PROFILE уже имеют собственные `prev...BlockNumber`, `prev...Hash32` и sequence. Эти линии — дополнительный способ выборочной загрузки и проверки поверх основной пользовательской цепочки. Если в выборочно скачанной линии обнаружена дырка, блоки всё равно можно криптографически проверить через глобальную цепочку, где уже есть `prevHash32 + back32Hash32`.
|
||||||
|
|
||||||
```text
|
Поэтому добавлять ещё +32 байта в каждый line-based body сейчас нецелесообразно. Если когда-нибудь появится требование полностью проверять отдельный канал/профиль без доступа к глобальной цепочке даже через недоступный line-блок, line-back32 можно рассмотреть отдельной новой версией body.
|
||||||
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`.
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ blockchain.sync.enabled=false
|
|||||||
|
|
||||||
Дополнительно каждый сервер может независимо включить `ArweaveBlockSyncScheduler`.
|
Дополнительно каждый сервер может независимо включить `ArweaveBlockSyncScheduler`.
|
||||||
|
|
||||||
Он ищет child DataItems по тестовому тегу `App=test5590`, проверяет их и импортирует через ту же бизнес-проверку AddBlock. Импортированные блоки не публикуются повторно.
|
Он ищет child DataItems по тестовому тегу `App=test5591`, проверяет их и импортирует через ту же бизнес-проверку AddBlock. Импортированные блоки не публикуются повторно.
|
||||||
|
|
||||||
Если блоки обнаружены не по порядку, persistent queue оставляет более поздние блоки pending до появления предыдущих.
|
Если блоки обнаружены не по порядку, persistent queue оставляет более поздние блоки pending до появления предыдущих.
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
} from './key-vault.js';
|
} from './key-vault.js';
|
||||||
import { createAns104DataItem, parseAns104DataItem } from './ans104-data-item.js';
|
import { createAns104DataItem, parseAns104DataItem } from './ans104-data-item.js';
|
||||||
import { defaultServerWs } from '../deploy-config.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 BCH_SUFFIX = '001';
|
||||||
const ZERO64 = '0'.repeat(64);
|
const ZERO64 = '0'.repeat(64);
|
||||||
@@ -896,7 +897,7 @@ function bytesToHexLower(bytes) {
|
|||||||
function parseStoredLineMeta(blockBytesB64, { fixedLine = false } = {}) {
|
function parseStoredLineMeta(blockBytesB64, { fixedLine = false } = {}) {
|
||||||
const raw = base64ToBytes(String(blockBytesB64 || ''));
|
const raw = base64ToBytes(String(blockBytesB64 || ''));
|
||||||
const frame = parseAns104DataItem(raw).data;
|
const frame = parseAns104DataItem(raw).data;
|
||||||
const bodyOffset = 56;
|
const bodyOffset = 88;
|
||||||
const minLength = fixedLine ? bodyOffset + 40 : bodyOffset + 44;
|
const minLength = fixedLine ? bodyOffset + 40 : bodyOffset + 44;
|
||||||
if (!(frame instanceof Uint8Array) || frame.length < minLength) throw new Error('Некорректный line-блок');
|
if (!(frame instanceof Uint8Array) || frame.length < minLength) throw new Error('Некорректный line-блок');
|
||||||
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
||||||
@@ -934,14 +935,16 @@ function isLineConflictError(error) {
|
|||||||
]).has(String(error?.code || '').toUpperCase());
|
]).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 prevHashBytes = hexToBytes(normalizeHex32(prevBlockHashHex));
|
||||||
|
const back32HashBytes = hexToBytes(normalizeHex32(back32HashHex, ZERO64));
|
||||||
const body = bodyBytes || new Uint8Array(0);
|
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(
|
return concatBytes(
|
||||||
int16Bytes(1),
|
int16Bytes(1),
|
||||||
prevHashBytes,
|
prevHashBytes,
|
||||||
|
back32HashBytes,
|
||||||
int32Bytes(blockSize),
|
int32Bytes(blockSize),
|
||||||
int32Bytes(blockNumber),
|
int32Bytes(blockNumber),
|
||||||
int64Bytes(Math.floor(Date.now() / 1000)),
|
int64Bytes(Math.floor(Date.now() / 1000)),
|
||||||
@@ -1621,7 +1624,7 @@ export class AuthService {
|
|||||||
owner32,
|
owner32,
|
||||||
privateKey,
|
privateKey,
|
||||||
data: preimage,
|
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', {
|
return this.ws.request('AddBlock', {
|
||||||
blockchainName: cleanBlockchainName,
|
blockchainName: cleanBlockchainName,
|
||||||
@@ -1753,7 +1756,13 @@ export class AuthService {
|
|||||||
const tryAdd = async (cursor) => {
|
const tryAdd = async (cursor) => {
|
||||||
const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1;
|
const blockNumber = Number(cursor?.serverLastGlobalNumber ?? -1) + 1;
|
||||||
const prevBlockHash = normalizeHex32(cursor?.serverLastGlobalHash, ZERO64);
|
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({
|
return this.submitPreparedAddBlock({
|
||||||
login,
|
login,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
@@ -1813,19 +1822,20 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
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);
|
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();
|
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({
|
const { response, blockchainName } = await this.runAddBlockWithRetry({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
|
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
|
||||||
ansTags,
|
ansTags,
|
||||||
buildPreimage: async ({ blockNumber, prevBlockHash }) => buildBlockPreimage({
|
buildPreimage: async ({ blockNumber, prevBlockHash, back32Hash }) => buildBlockPreimage({
|
||||||
prevBlockHashHex: prevBlockHash,
|
prevBlockHashHex: prevBlockHash,
|
||||||
|
back32HashHex: back32Hash,
|
||||||
blockNumber,
|
blockNumber,
|
||||||
msgType,
|
msgType,
|
||||||
msgSubType,
|
msgSubType,
|
||||||
@@ -2199,26 +2209,20 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getBlockHashByNumber(blockchainName, blockNumber) {
|
async getBlockHashByNumber(blockchainName, blockNumber) {
|
||||||
|
const cleanBlockchainName = String(blockchainName || '').trim();
|
||||||
const cleanBlockNumber = Number(blockNumber);
|
const cleanBlockNumber = Number(blockNumber);
|
||||||
try {
|
if (!cleanBlockchainName || !Number.isInteger(cleanBlockNumber) || cleanBlockNumber < 0) {
|
||||||
const payload = await this.getMessageThread(
|
throw new Error('Некорректный blockchainName/blockNumber для GetBlockchainBlock');
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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) {
|
async resolveHeaderHashForBlockchain(blockchainName) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createAns104DataItem, createAns104DataItemWithRawParts, parseAns104Data
|
|||||||
import { base64ToBytes, bytesToBase64 } from './crypto-utils.js';
|
import { base64ToBytes, bytesToBase64 } from './crypto-utils.js';
|
||||||
import { updateShineUserPdaOnSolana } from './shine-user-pda-service.js';
|
import { updateShineUserPdaOnSolana } from './shine-user-pda-service.js';
|
||||||
import { updateEncryptedUserSecrets } from './key-vault.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 TE = new TextEncoder();
|
||||||
const ZERO_HASH = '0'.repeat(64);
|
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 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 normalizeComment(value) { return String(value || '').trim().replace(/\r\n/g,'\n').replace(/\r/g,'\n'); }
|
||||||
function readFrameTimestampMs(frame) {
|
function readFrameTimestampMs(frame) {
|
||||||
if (!(frame instanceof Uint8Array) || frame.length < 50) throw new Error('Некорректный SHiNE Frame');
|
if (!(frame instanceof Uint8Array) || frame.length < 82) throw new Error('Некорректный SHiNE Frame');
|
||||||
const seconds = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getBigInt64(42, false);
|
const seconds = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getBigInt64(74, false);
|
||||||
return Number(seconds * 1000n);
|
return Number(seconds * 1000n);
|
||||||
}
|
}
|
||||||
function readFrameType(frame) {
|
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);
|
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) {
|
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);
|
const view=new DataView(frame.buffer,frame.byteOffset,frame.byteLength);
|
||||||
return {
|
return {
|
||||||
lineCode:view.getInt32(56,false),
|
lineCode:view.getInt32(88,false),
|
||||||
prevLineNumber:view.getInt32(60,false),
|
prevLineNumber:view.getInt32(92,false),
|
||||||
prevLineHash:bytesToHex(frame.slice(64,96)),
|
prevLineHash:bytesToHex(frame.slice(96,128)),
|
||||||
lineSequence:view.getInt32(96,false),
|
lineSequence:view.getInt32(128,false),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function makeForkBody({ lineCode=0, prevLineNumber, prevLineHash, lineSequence, oldBlockchainKeyB64, forkPointBlock, forkPointHash, forkPointTimestampMs, parentTipBlock, parentTipHash, parentTipTimestampMs, reasonCode, comment }) {
|
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);
|
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 bodyBytes=body || new Uint8Array(0);
|
||||||
const size=2+32+4+4+8+2+2+2+bodyBytes.length;
|
const size=2+32+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);
|
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([
|
export const KEY_ROTATION_REASONS = Object.freeze([
|
||||||
@@ -153,8 +154,10 @@ export class KeyRotationClient {
|
|||||||
comment:rotation.comment,
|
comment:rotation.comment,
|
||||||
});
|
});
|
||||||
const techTimestampMs=Number(rotation.createdAtMs)||readFrameTimestampMs(tipParsed.data)+1000;
|
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 techBlockNumber=cutoff+1;
|
||||||
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:'App',value:'test5590'},{name:'t',value:'tech'}] });
|
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) });
|
await this.addCandidate({ blockNumber:cutoff+1, prevBlockHash:rotation.forkFromHash, blockBytesB64:bytesToBase64(techRaw) });
|
||||||
onProgress({ phase:'copy', current:total, total });
|
onProgress({ phase:'copy', current:total, total });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
&solanaRpc=https://api.mainnet-beta.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5590` и строит список каналов из найденных блоков.
|
`channel` можно не передавать. Тогда viewer ищет все DataItem пользователя с `App=test5591` и строит список каналов из найденных блоков.
|
||||||
|
|
||||||
## Что делает viewer
|
## Что делает viewer
|
||||||
|
|
||||||
1. Если передан `login`, вычисляет PDA `user_login=<login>` программы `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6` и читает `blockchainKey`.
|
1. Если передан `login`, вычисляет PDA `user_login=<login>` программы `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6` и читает `blockchainKey`.
|
||||||
2. Если передан `key`, использует его напрямую и не обращается к Solana.
|
2. Если передан `key`, использует его напрямую и не обращается к Solana.
|
||||||
3. Из Ed25519/Solana public key вычисляет Arweave owner address как `base64url(SHA-256(pubkey32))`.
|
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>`.
|
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.
|
7. Дедуплицирует повторно опубликованные одинаковые пользовательские блоки по SHA-256 Frame v1.
|
||||||
8. Восстанавливает канал/сообщения тем же body parser, который использовал старый viewer.
|
8. Восстанавливает канал/сообщения тем же body parser, который использовал старый viewer.
|
||||||
|
|
||||||
|
|||||||
@@ -1097,9 +1097,12 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
const BLOCK_TYPE_SESSIONS = 50;
|
const BLOCK_TYPE_SESSIONS = 50;
|
||||||
const BLOCK_TYPE_TRUSTED_STATE = 70;
|
const BLOCK_TYPE_TRUSTED_STATE = 70;
|
||||||
const BLOCK_TYPE_ARCHIVE_HEAD = 80;
|
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 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_TECH = 0;
|
||||||
const BCH_TYPE_TEXT = 1;
|
const BCH_TYPE_TEXT = 1;
|
||||||
const BCH_SUBTYPE_HEADER = 0;
|
const BCH_SUBTYPE_HEADER = 0;
|
||||||
@@ -2058,22 +2061,23 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
if (frameCode !== BCH_FRAME_CODE_V1) {
|
if (frameCode !== BCH_FRAME_CODE_V1) {
|
||||||
throw new Error(`Неожиданный frameCode 0x${frameCode.toString(16)} (ожидался 0x0001)`);
|
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) {
|
if (blockSize !== bytes.length || blockSize < BCH_FRAME_HEADER_SIZE) {
|
||||||
throw new Error(`Frame size mismatch: blockSize=${blockSize}, actual=${bytes.length}`);
|
throw new Error(`Frame size mismatch: blockSize=${blockSize}, actual=${bytes.length}`);
|
||||||
}
|
}
|
||||||
const hashBytes = await digestSha256(bytes);
|
const hashBytes = await digestSha256(bytes);
|
||||||
const type = readUint16BE(bytes, 50);
|
const type = readUint16BE(bytes, 82);
|
||||||
const subType = readUint16BE(bytes, 52);
|
const subType = readUint16BE(bytes, 84);
|
||||||
const version = readUint16BE(bytes, 54);
|
const version = readUint16BE(bytes, 86);
|
||||||
const bodyBytes = bytes.slice(BCH_FRAME_HEADER_SIZE);
|
const bodyBytes = bytes.slice(BCH_FRAME_HEADER_SIZE);
|
||||||
const body = parseBlockchainBody(type, subType, version, bodyBytes);
|
const body = parseBlockchainBody(type, subType, version, bodyBytes);
|
||||||
return {
|
return {
|
||||||
frameCode,
|
frameCode,
|
||||||
prevHashHex: bytesToHex(bytes.slice(2, 34)),
|
prevHashHex: bytesToHex(bytes.slice(2, 34)),
|
||||||
|
back32HashHex: bytesToHex(bytes.slice(34, 66)),
|
||||||
blockSize,
|
blockSize,
|
||||||
blockNumber: readInt32BE(bytes, 38),
|
blockNumber: readInt32BE(bytes, 70),
|
||||||
timestampSec: readInt64BE(bytes, 42),
|
timestampSec: readInt64BE(bytes, 74),
|
||||||
type,
|
type,
|
||||||
subType,
|
subType,
|
||||||
version,
|
version,
|
||||||
@@ -2501,7 +2505,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (offset < data.length) {
|
while (offset < data.length) {
|
||||||
if (data.length - offset < BCH_FRAME_HEADER_SIZE) throw new Error(`Frame обрывается на смещении ${offset}`);
|
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}`);
|
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)));
|
blocks.push(await parseBlockchainBlock(data.slice(offset, offset + blockSize)));
|
||||||
offset += blockSize;
|
offset += blockSize;
|
||||||
@@ -3345,7 +3349,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
const oldSetContentStatus = setContentStatus;
|
const oldSetContentStatus = setContentStatus;
|
||||||
setContentStatus = function(kind,text){ oldSetContentStatus(kind,text); const el=document.getElementById('v3-status'); if(el) el.textContent=text; v3Log(text); };
|
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 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(); }
|
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);} }
|
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);}
|
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){
|
function v3Verification(blocks){
|
||||||
blocks=(blocks||[]).slice().sort((a,b)=>a.blockNumber-b.blockNumber);
|
blocks=(blocks||[]).slice().sort((a,b)=>a.blockNumber-b.blockNumber);
|
||||||
const byNum=new Map(blocks.map(b=>[Number(b.blockNumber),b]));
|
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){
|
for(const b of blocks){
|
||||||
// hashHex is SHA-256(Frame) calculated locally by parseBlockchainBlock.
|
// hashHex is SHA-256(Frame) calculated locally by parseBlockchainBlock.
|
||||||
if(!b.hashHex){result.frameHash.ok--;result.frameHash.na++;}
|
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 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}`});}
|
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||{};
|
const body=b.body||{};
|
||||||
if(body.kind==='text_line' || body.kind==='create_channel'){
|
if(body.kind==='text_line' || body.kind==='create_channel'){
|
||||||
const pn=Number(body.prevLineNumber||0), ph=String(body.prevLineHashHex||'');
|
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(){
|
function v3UpdateVerification(){
|
||||||
const r=v3Verification(currentBlockchainView?.rawBlocks||V3.blocks||[]);
|
const r=v3Verification(currentBlockchainView?.rawBlocks||V3.blocks||[]);
|
||||||
V3.verification=r;
|
V3.verification=r;
|
||||||
const bad=r.global.bad+r.channel.bad+r.frameHash.bad+r.owner.bad+r.signature.bad;
|
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.channel.ok;
|
const checked=r.global.ok+r.back32.ok+r.channel.ok;
|
||||||
const el=document.getElementById('v3-status');
|
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)}`;
|
if(el && r.total) el.textContent=`${bad?'⚠':'✓'} Хэши ${checked} · global ${v3MetricText(r.global)} · канал ${v3MetricText(r.channel)} · owner ${v3MetricText(r.owner)}`;
|
||||||
return r;
|
return r;
|
||||||
@@ -3416,6 +3430,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
document.getElementById('v3-verify-summary').innerHTML=
|
document.getElementById('v3-verify-summary').innerHTML=
|
||||||
v3VerifyCard('SHA-256 Frame',r.frameHash,'вычислен локально')+
|
v3VerifyCard('SHA-256 Frame',r.frameHash,'вычислен локально')+
|
||||||
v3VerifyCard('Общая цепочка',r.global,'prevHash')+
|
v3VerifyCard('Общая цепочка',r.global,'prevHash')+
|
||||||
|
v3VerifyCard('Резерв N−32',r.back32,'back32Hash')+
|
||||||
v3VerifyCard('Цепочки каналов',r.channel,'prevLineHash')+
|
v3VerifyCard('Цепочки каналов',r.channel,'prevLineHash')+
|
||||||
v3VerifyCard('Arweave owner',r.owner,'совпадение выборки owner');
|
v3VerifyCard('Arweave owner',r.owner,'совпадение выборки owner');
|
||||||
const sig=document.getElementById('v3-signature-note');
|
const sig=document.getElementById('v3-signature-note');
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
@@ -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`
|
|
||||||
@@ -35,7 +35,7 @@ JWK используется только подготовщиком. Не по
|
|||||||
- `prevHash` глобальной цепочки.
|
- `prevHash` глобальной цепочки.
|
||||||
- `prevLineNumber/prevLineHash` канальной линии.
|
- `prevLineNumber/prevLineHash` канальной линии.
|
||||||
- ANS-104 signing/deepHash без сверки с серверной реализацией.
|
- ANS-104 signing/deepHash без сверки с серверной реализацией.
|
||||||
- `App=test5590` и `c=<canonical channel>` без согласованного изменения импортера/Viewer.
|
- текущий `App=<SHINE_PROTOCOL_NAMESPACE>` и `c=<canonical channel>` без согласованного изменения импортера/Viewer.
|
||||||
|
|
||||||
## Viewer
|
## Viewer
|
||||||
Viewer распознаёт Arweave URL в тексте POST и показывает изображения inline. Экран «Проверка данных» независимо считает SHA-256 Frame, проверяет доступные `prevHash` и `prevLineHash`, показывает отсутствие соседнего блока как «нет данных», а доказанное несовпадение — как ошибку. Подпись ANS-104 нельзя помечать локально проверенной, если gateway не предоставил сырую подпись DataItem.
|
Viewer распознаёт Arweave URL в тексте POST и показывает изображения inline. Экран «Проверка данных» независимо считает SHA-256 Frame, проверяет доступные `prevHash` и `prevLineHash`, показывает отсутствие соседнего блока как «нет данных», а доказанное несовпадение — как ошибку. Подпись ANS-104 нельзя помечать локально проверенной, если gateway не предоставил сырую подпись DataItem.
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -1,11 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
from websockets.asyncio.client import connect
|
from websockets.asyncio.client import connect
|
||||||
|
|
||||||
ZERO32 = bytes(32)
|
ZERO32 = bytes(32)
|
||||||
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
|
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):
|
def atomic_json(path, obj):
|
||||||
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
|
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')
|
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
|
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):
|
def frame(prev_hash, back32_hash, block_num, body, ts=None):
|
||||||
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
|
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
|
||||||
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+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 h32(b): return hashlib.sha256(b).digest()
|
||||||
def hx(b): return b.hex()
|
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
|
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
|
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)
|
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)
|
raw=data_item(read_key(cfg['key_file']),tags,fr)
|
||||||
nextidx=idx+1
|
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()}
|
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,11 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
from websockets.asyncio.client import connect
|
from websockets.asyncio.client import connect
|
||||||
|
|
||||||
ZERO32 = bytes(32)
|
ZERO32 = bytes(32)
|
||||||
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
|
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):
|
def atomic_json(path, obj):
|
||||||
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
|
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')
|
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
|
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):
|
def frame(prev_hash, back32_hash, block_num, body, ts=None):
|
||||||
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
|
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
|
||||||
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+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 h32(b): return hashlib.sha256(b).digest()
|
||||||
def hx(b): return b.hex()
|
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
|
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
|
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)
|
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)
|
raw=data_item(read_key(cfg['key_file']),tags,fr)
|
||||||
nextidx=idx+1
|
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()}
|
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
|
#!/usr/bin/env python3
|
||||||
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
import argparse, asyncio, base64, hashlib, json, os, random, struct, sys, time, uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
@@ -8,6 +9,18 @@ from websockets.asyncio.client import connect
|
|||||||
|
|
||||||
ZERO32 = bytes(32)
|
ZERO32 = bytes(32)
|
||||||
FRAME_CODE_V1=1; TEXT_TYPE=1; TEXT_POST=10; VERSION=1
|
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):
|
def atomic_json(path, obj):
|
||||||
path=Path(path); path.parent.mkdir(parents=True, exist_ok=True)
|
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')
|
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
|
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):
|
def frame(prev_hash, back32_hash, block_num, body, ts=None):
|
||||||
ts=int(time.time()) if ts is None else int(ts); size=56+len(body)
|
ts=int(time.time()) if ts is None else int(ts); size=88+len(body)
|
||||||
return struct.pack('>H32siiqHHH',FRAME_CODE_V1,prev_hash,size,block_num,ts,TEXT_TYPE,TEXT_POST,VERSION)+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 h32(b): return hashlib.sha256(b).digest()
|
||||||
def hx(b): return b.hex()
|
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
|
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
|
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)
|
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)
|
raw=data_item(read_key(cfg['key_file']),tags,fr)
|
||||||
nextidx=idx+1
|
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()}
|
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']))
|
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)
|
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)
|
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)
|
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,
|
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'],
|
'block_hash':hx(block_hash),'previous_block_number':st['last_block_number'],'previous_block_hash':st['last_block_hash'],
|
||||||
|
|||||||
Reference in New Issue
Block a user