SHA256
Добавить линии связей и параметров пользователя
This commit is contained in:
@@ -13,5 +13,5 @@
|
|||||||
// public static final short TEXT = 1; // сообщения да надо
|
// public static final short TEXT = 1; // сообщения да надо
|
||||||
// public static final short REACTION = 2; // реакции не надо
|
// public static final short REACTION = 2; // реакции не надо
|
||||||
// public static final short CONNECTION = 3; // связи (friend/contact/follow) да надо
|
// public static final short CONNECTION = 3; // связи (friend/contact/follow) да надо
|
||||||
// public static final short USER_PARAM = 4; // параметры профиля да надо
|
// public static final short USER_PROFILE= 4; // профиль пользователя да надо
|
||||||
//}
|
//}
|
||||||
+13
-11
@@ -1,21 +1,23 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BodyHasLine — для типов, которые имеют линейные поля в body.
|
* BodyHasLine — типы блоков, которые входят в проверяемую логическую линию.
|
||||||
*
|
*
|
||||||
* Line-prefix (BigEndian) в НАЧАЛЕ bodyBytes:
|
* Общая семантика:
|
||||||
* [4] lineCode код линии (root-идентификатор):
|
* prevLineBlockGlobalNumber — GLOBAL blockNumber предыдущего блока этой линии;
|
||||||
* - 0 для дефолтной линии/канала "0" (root = HEADER, blockNumber=0)
|
* prevLineBlockHash32 — hash32 предыдущего блока этой линии;
|
||||||
* - для канала "X": blockNumber root-блока канала (CREATE_CHANNEL)
|
* lineSeq — порядковый номер шага линии.
|
||||||
*
|
*
|
||||||
* [4] prevLineBlockGlobalNumber глобальный номер предыдущего блока в этой линии
|
* lineCode — логический идентификатор линии:
|
||||||
* [32] prevLineBlockHash32 hash32 предыдущего блока в этой линии
|
* - 0 для фиксированных системных линий (TECH, CONNECTION, USER_PROFILE)
|
||||||
|
* и для дефолтного канала, корнем которого является HEADER #0;
|
||||||
|
* - для пользовательского канала: GLOBAL blockNumber его CREATE_CHANNEL root.
|
||||||
*
|
*
|
||||||
* [4] lineSeq порядковый номер сообщения внутри линии (1..N)
|
* Важно: lineCode не обязан физически присутствовать в body. Для фиксированных
|
||||||
|
* USER_PROFILE и CONNECTION он выводится из msg_type и метод lineCode() возвращает 0.
|
||||||
|
* Для TEXT-каналов и TECH-блоков lineCode хранится в body явно.
|
||||||
*
|
*
|
||||||
* Важно:
|
* Полная проверка хвоста линии, prev hash и lineSeq выполняется сервером при AddBlock.
|
||||||
* - Проверка связности линии (prevLineBlockGlobalNumber ↔ prevLineBlockHash32) и корректности lineSeq
|
|
||||||
* выполняется на сервере/в БД при вставке (а не в body.check()).
|
|
||||||
*/
|
*/
|
||||||
public interface BodyHasLine {
|
public interface BodyHasLine {
|
||||||
|
|
||||||
|
|||||||
+39
-26
@@ -7,7 +7,10 @@ import java.nio.ByteOrder;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/** CONNECTION with line fields and canonical external target. */
|
/**
|
||||||
|
* CONNECTION with one fixed connection-history line and canonical external target.
|
||||||
|
* lineCode is not stored in body: the logical connection line always has code 0.
|
||||||
|
*/
|
||||||
public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||||
public static final short TYPE = 3;
|
public static final short TYPE = 3;
|
||||||
public static final short VER = 1;
|
public static final short VER = 1;
|
||||||
@@ -15,10 +18,9 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|
|
||||||
public final short subType;
|
public final short subType;
|
||||||
public final short version;
|
public final short version;
|
||||||
public final int lineCode;
|
public final int prevConnectionBlockNumber;
|
||||||
public final int prevLineNumber;
|
public final byte[] prevConnectionBlockHash32;
|
||||||
public final byte[] prevLineHash32;
|
public final int connectionSequence;
|
||||||
public final int thisLineNumber;
|
|
||||||
public final String toLogin;
|
public final String toLogin;
|
||||||
public final int toForkNumber;
|
public final int toForkNumber;
|
||||||
public final int toBlockGlobalNumber;
|
public final int toBlockGlobalNumber;
|
||||||
@@ -26,47 +28,59 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|
|
||||||
public ConnectionBody(short subType, short version, byte[] bodyBytes) {
|
public ConnectionBody(short subType, short version, byte[] bodyBytes) {
|
||||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
this.subType = subType; this.version = version;
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
if ((version & 0xFFFF) != 1 || !isValidSubType(subType)) throw new IllegalArgumentException("Bad Connection header");
|
if ((version & 0xFFFF) != 1 || !isValidSubType(subType)) throw new IllegalArgumentException("Bad Connection header");
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
BodyTargetSupport.ensureMin(bb, 4 + 4 + 32 + 4 + 1 + 1 + 2 + 4 + 32, "ConnectionBody too short");
|
BodyTargetSupport.ensureMin(bb, 4 + 32 + 4 + 1 + 1 + 2 + 4 + 32, "ConnectionBody too short");
|
||||||
this.lineCode = bb.getInt(); this.prevLineNumber = bb.getInt(); this.prevLineHash32 = new byte[32]; bb.get(this.prevLineHash32); this.thisLineNumber = bb.getInt();
|
this.prevConnectionBlockNumber = bb.getInt();
|
||||||
|
this.prevConnectionBlockHash32 = new byte[32];
|
||||||
|
bb.get(this.prevConnectionBlockHash32);
|
||||||
|
this.connectionSequence = bb.getInt();
|
||||||
BodyTargetSupport.ExternalTarget t = BodyTargetSupport.readExternal(bb, "CONNECTION", 0);
|
BodyTargetSupport.ExternalTarget t = BodyTargetSupport.readExternal(bb, "CONNECTION", 0);
|
||||||
this.toLogin = t.login(); this.toForkNumber = t.forkNumber(); this.toBlockGlobalNumber = t.blockNumber(); this.toBlockHash32 = t.blockHash32();
|
this.toLogin = t.login();
|
||||||
|
this.toForkNumber = t.forkNumber();
|
||||||
|
this.toBlockGlobalNumber = t.blockNumber();
|
||||||
|
this.toBlockHash32 = t.blockHash32();
|
||||||
if (bb.hasRemaining()) throw new IllegalArgumentException("Unexpected ConnectionBody tail");
|
if (bb.hasRemaining()) throw new IllegalArgumentException("Unexpected ConnectionBody tail");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionBody(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, short subType,
|
public ConnectionBody(int prevConnectionBlockNumber, byte[] prevConnectionBlockHash32, int connectionSequence, short subType,
|
||||||
String toLogin, int toForkNumber, int toBlockGlobalNumber, byte[] toBlockHash32) {
|
String toLogin, int toForkNumber, int toBlockGlobalNumber, byte[] toBlockHash32) {
|
||||||
if (lineCode < 0 || !isValidSubType(subType)) throw new IllegalArgumentException("Bad connection fields");
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection fields");
|
||||||
BodyTargetSupport.validateExternal(toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
BodyTargetSupport.validateExternal(toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
||||||
this.lineCode = lineCode; this.prevLineNumber = prevLineNumber;
|
this.prevConnectionBlockNumber = prevConnectionBlockNumber;
|
||||||
this.prevLineHash32 = prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32);
|
this.prevConnectionBlockHash32 = prevConnectionBlockHash32 == null ? new byte[32] : Arrays.copyOf(prevConnectionBlockHash32, 32);
|
||||||
this.thisLineNumber = thisLineNumber; this.subType = subType; this.version = VER;
|
this.connectionSequence = connectionSequence;
|
||||||
this.toLogin = toLogin; this.toForkNumber = toForkNumber; this.toBlockGlobalNumber = toBlockGlobalNumber; this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
this.toLogin = toLogin;
|
||||||
|
this.toForkNumber = toForkNumber;
|
||||||
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public ConnectionBody check() {
|
@Override public ConnectionBody check() {
|
||||||
if (lineCode < 0 || !isValidSubType(subType)) throw new IllegalArgumentException("Bad connection fields");
|
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection fields");
|
||||||
if (prevLineNumber == -1) {
|
if (prevConnectionBlockNumber < 0) throw new IllegalArgumentException("prevConnectionBlockNumber must be >=0");
|
||||||
if (!isAllZero32(prevLineHash32) || thisLineNumber != -1) throw new IllegalArgumentException("Bad empty connection line anchor");
|
if (prevConnectionBlockHash32 == null || prevConnectionBlockHash32.length != 32) throw new IllegalArgumentException("prevConnectionBlockHash32 invalid");
|
||||||
} else if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
if (connectionSequence <= 0) throw new IllegalArgumentException("connectionSequence must be >=1");
|
||||||
BodyTargetSupport.validateExternal(toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
BodyTargetSupport.validateExternal(toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public byte[] toBytes() {
|
@Override public byte[] toBytes() {
|
||||||
check();
|
check();
|
||||||
ByteBuffer bb = ByteBuffer.allocate(4 + 4 + 32 + 4 + BodyTargetSupport.externalSize(toLogin)).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(4 + 32 + 4 + BodyTargetSupport.externalSize(toLogin)).order(ByteOrder.BIG_ENDIAN);
|
||||||
bb.putInt(lineCode).putInt(prevLineNumber).put(prevLineHash32).putInt(thisLineNumber);
|
bb.putInt(prevConnectionBlockNumber).put(prevConnectionBlockHash32).putInt(connectionSequence);
|
||||||
BodyTargetSupport.writeExternal(bb, toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
BodyTargetSupport.writeExternal(bb, toLogin, toForkNumber, toBlockGlobalNumber, toBlockHash32, "CONNECTION");
|
||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public int lineCode() { return lineCode; }
|
@Override public int lineCode() { return 0; }
|
||||||
@Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
|
@Override public int prevLineBlockGlobalNumber() { return prevConnectionBlockNumber; }
|
||||||
@Override public byte[] prevLineBlockHash32() { return Arrays.copyOf(prevLineHash32, 32); }
|
@Override public byte[] prevLineBlockHash32() { return Arrays.copyOf(prevConnectionBlockHash32, 32); }
|
||||||
@Override public int lineSeq() { return thisLineNumber; }
|
@Override public int lineSeq() { return connectionSequence; }
|
||||||
@Override public String toLogin() { return toLogin; }
|
@Override public String toLogin() { return toLogin; }
|
||||||
@Override public Integer toForkNumber() { return toForkNumber; }
|
@Override public Integer toForkNumber() { return toForkNumber; }
|
||||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
@@ -87,5 +101,4 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|| v == (MsgSubType.CONNECTION_SHINE_SEEN & 0xFFFF) || v == (MsgSubType.CONNECTION_SHINE_UNSEEN & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_SHINE_SEEN & 0xFFFF) || v == (MsgSubType.CONNECTION_SHINE_UNSEEN & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF) || v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED & 0xFFFF);
|
|| v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF) || v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED & 0xFFFF);
|
||||||
}
|
}
|
||||||
private static boolean isAllZero32(byte[] b) { if (b == null || b.length != 32) return false; for (byte v : b) if (v != 0) return false; return true; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import java.util.Objects;
|
|||||||
/**
|
/**
|
||||||
* TECH_FORK body (type=0, subType=2, version=1).
|
* TECH_FORK body (type=0, subType=2, version=1).
|
||||||
*
|
*
|
||||||
* Записывается первым новым блоком после точной перепубликации выбранного
|
* TECH_FORK является обычным шагом общей TECH-линии. Поэтому body начинается
|
||||||
* префикса старой цепочки новым blockchain key.
|
* тем же line-prefix, что и TECH_CREATE_CHANNEL.
|
||||||
*
|
*
|
||||||
* body bytes (BigEndian):
|
* body bytes (BigEndian):
|
||||||
|
* [4] lineCode (=0 для TECH-линии)
|
||||||
|
* [4] prevLineNumber
|
||||||
|
* [32] prevLineHash32
|
||||||
|
* [4] thisLineNumber
|
||||||
|
*
|
||||||
* [32] parentBlockchainKey
|
* [32] parentBlockchainKey
|
||||||
* [4] forkPointBlockNumber
|
* [4] forkPointBlockNumber
|
||||||
* [32] forkPointBlockHash32
|
* [32] forkPointBlockHash32
|
||||||
@@ -27,7 +32,7 @@ import java.util.Objects;
|
|||||||
* [2] commentUtf8Length
|
* [2] commentUtf8Length
|
||||||
* [N] comment UTF-8 (0..1024 bytes)
|
* [N] comment UTF-8 (0..1024 bytes)
|
||||||
*/
|
*/
|
||||||
public final class ForkBody implements BodyRecord {
|
public final class ForkBody implements BodyRecord, BodyHasLine {
|
||||||
|
|
||||||
public static final short TYPE = 0;
|
public static final short TYPE = 0;
|
||||||
public static final short VER = 1;
|
public static final short VER = 1;
|
||||||
@@ -40,10 +45,18 @@ public final class ForkBody implements BodyRecord {
|
|||||||
public static final int REASON_RECOVERY = 4;
|
public static final int REASON_RECOVERY = 4;
|
||||||
|
|
||||||
public static final int MAX_COMMENT_UTF8_LEN = 1024;
|
public static final int MAX_COMMENT_UTF8_LEN = 1024;
|
||||||
private static final int FIXED_LEN = 32 + 4 + 32 + 8 + 4 + 32 + 8 + 4 + 1 + 2;
|
private static final int LINE_PREFIX_LEN = 4 + 4 + 32 + 4;
|
||||||
|
private static final int FIXED_PAYLOAD_LEN = 32 + 4 + 32 + 8 + 4 + 32 + 8 + 4 + 1 + 2;
|
||||||
|
private static final int FIXED_LEN = LINE_PREFIX_LEN + FIXED_PAYLOAD_LEN;
|
||||||
|
|
||||||
public final short subType;
|
public final short subType;
|
||||||
public final short version;
|
public final short version;
|
||||||
|
|
||||||
|
public final int lineCode;
|
||||||
|
public final int prevLineNumber;
|
||||||
|
public final byte[] prevLineHash32;
|
||||||
|
public final int thisLineNumber;
|
||||||
|
|
||||||
public final byte[] parentBlockchainKey32;
|
public final byte[] parentBlockchainKey32;
|
||||||
public final int forkPointBlockNumber;
|
public final int forkPointBlockNumber;
|
||||||
public final byte[] forkPointBlockHash32;
|
public final byte[] forkPointBlockHash32;
|
||||||
@@ -71,6 +84,12 @@ public final class ForkBody implements BodyRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
this.lineCode = bb.getInt();
|
||||||
|
this.prevLineNumber = bb.getInt();
|
||||||
|
this.prevLineHash32 = new byte[32];
|
||||||
|
bb.get(this.prevLineHash32);
|
||||||
|
this.thisLineNumber = bb.getInt();
|
||||||
|
|
||||||
this.parentBlockchainKey32 = new byte[32];
|
this.parentBlockchainKey32 = new byte[32];
|
||||||
bb.get(this.parentBlockchainKey32);
|
bb.get(this.parentBlockchainKey32);
|
||||||
this.forkPointBlockNumber = bb.getInt();
|
this.forkPointBlockNumber = bb.getInt();
|
||||||
@@ -95,7 +114,11 @@ public final class ForkBody implements BodyRecord {
|
|||||||
this.comment = new String(commentBytes, StandardCharsets.UTF_8);
|
this.comment = new String(commentBytes, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ForkBody(byte[] parentBlockchainKey32,
|
public ForkBody(int lineCode,
|
||||||
|
int prevLineNumber,
|
||||||
|
byte[] prevLineHash32,
|
||||||
|
int thisLineNumber,
|
||||||
|
byte[] parentBlockchainKey32,
|
||||||
int forkPointBlockNumber,
|
int forkPointBlockNumber,
|
||||||
byte[] forkPointBlockHash32,
|
byte[] forkPointBlockHash32,
|
||||||
long forkPointTimestampMs,
|
long forkPointTimestampMs,
|
||||||
@@ -107,6 +130,10 @@ public final class ForkBody implements BodyRecord {
|
|||||||
String comment) {
|
String comment) {
|
||||||
this.subType = SUBTYPE;
|
this.subType = SUBTYPE;
|
||||||
this.version = VER;
|
this.version = VER;
|
||||||
|
this.lineCode = lineCode;
|
||||||
|
this.prevLineNumber = prevLineNumber;
|
||||||
|
this.prevLineHash32 = copy32(prevLineHash32, "prevLineHash32");
|
||||||
|
this.thisLineNumber = thisLineNumber;
|
||||||
this.parentBlockchainKey32 = copy32(parentBlockchainKey32, "parentBlockchainKey32");
|
this.parentBlockchainKey32 = copy32(parentBlockchainKey32, "parentBlockchainKey32");
|
||||||
this.forkPointBlockNumber = forkPointBlockNumber;
|
this.forkPointBlockNumber = forkPointBlockNumber;
|
||||||
this.forkPointBlockHash32 = copy32(forkPointBlockHash32, "forkPointBlockHash32");
|
this.forkPointBlockHash32 = copy32(forkPointBlockHash32, "forkPointBlockHash32");
|
||||||
@@ -127,6 +154,11 @@ public final class ForkBody implements BodyRecord {
|
|||||||
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
throw new IllegalArgumentException("ForkBody version must be 1");
|
throw new IllegalArgumentException("ForkBody version must be 1");
|
||||||
}
|
}
|
||||||
|
if (lineCode != 0) throw new IllegalArgumentException("ForkBody lineCode must be 0");
|
||||||
|
if (prevLineNumber < 0) throw new IllegalArgumentException("prevLineNumber must be >=0");
|
||||||
|
require32(prevLineHash32, "prevLineHash32");
|
||||||
|
if (thisLineNumber <= 0) throw new IllegalArgumentException("thisLineNumber must be >=1");
|
||||||
|
|
||||||
require32(parentBlockchainKey32, "parentBlockchainKey32");
|
require32(parentBlockchainKey32, "parentBlockchainKey32");
|
||||||
require32(forkPointBlockHash32, "forkPointBlockHash32");
|
require32(forkPointBlockHash32, "forkPointBlockHash32");
|
||||||
require32(parentTipBlockHash32, "parentTipBlockHash32");
|
require32(parentTipBlockHash32, "parentTipBlockHash32");
|
||||||
@@ -155,6 +187,10 @@ public final class ForkBody implements BodyRecord {
|
|||||||
check();
|
check();
|
||||||
byte[] commentUtf8 = normalizeComment(comment).getBytes(StandardCharsets.UTF_8);
|
byte[] commentUtf8 = normalizeComment(comment).getBytes(StandardCharsets.UTF_8);
|
||||||
ByteBuffer bb = ByteBuffer.allocate(FIXED_LEN + commentUtf8.length).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(FIXED_LEN + commentUtf8.length).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.putInt(lineCode);
|
||||||
|
bb.putInt(prevLineNumber);
|
||||||
|
bb.put(prevLineHash32);
|
||||||
|
bb.putInt(thisLineNumber);
|
||||||
bb.put(parentBlockchainKey32);
|
bb.put(parentBlockchainKey32);
|
||||||
bb.putInt(forkPointBlockNumber);
|
bb.putInt(forkPointBlockNumber);
|
||||||
bb.put(forkPointBlockHash32);
|
bb.put(forkPointBlockHash32);
|
||||||
@@ -169,6 +205,11 @@ public final class ForkBody implements BodyRecord {
|
|||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public int lineCode() { return lineCode; }
|
||||||
|
@Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
|
||||||
|
@Override public byte[] prevLineBlockHash32() { return Arrays.copyOf(prevLineHash32, 32); }
|
||||||
|
@Override public int lineSeq() { return thisLineNumber; }
|
||||||
|
|
||||||
public static boolean isReasonSupported(int code) {
|
public static boolean isReasonSupported(int code) {
|
||||||
return code == REASON_ROUTINE_ROTATION
|
return code == REASON_ROUTINE_ROTATION
|
||||||
|| code == REASON_POSSIBLE_COMPROMISE
|
|| code == REASON_POSSIBLE_COMPROMISE
|
||||||
|
|||||||
+37
-63
@@ -11,16 +11,16 @@ import java.util.Arrays;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserParamBody — type=4, ver=1 (в заголовке блока).
|
* UserParamBody — type=4, ver=1 (в документации: USER_PROFILE).
|
||||||
*
|
*
|
||||||
* subType (в заголовке блока):
|
* subType (в заголовке блока):
|
||||||
* 1 = TEXT_TEXT
|
* 1 = TEXT_TEXT
|
||||||
*
|
*
|
||||||
* bodyBytes (BigEndian), новый формат:
|
* USER_PROFILE имеет единственную фиксированную линию, поэтому lineCode в body не хранится.
|
||||||
* [4] lineCode
|
* bodyBytes (BigEndian):
|
||||||
* [4] prevLineNumber
|
* [4] prevProfileBlockNumber — GLOBAL blockNumber предыдущего USER_PROFILE-блока
|
||||||
* [32] prevLineHash32
|
* [32] prevProfileBlockHash32 — hash32 предыдущего USER_PROFILE-блока
|
||||||
* [4] thisLineNumber
|
* [4] profileSequence — порядковый номер изменения профиля (1..N)
|
||||||
*
|
*
|
||||||
* [2] keyLenBytes (uint16)
|
* [2] keyLenBytes (uint16)
|
||||||
* [N] keyUtf8
|
* [N] keyUtf8
|
||||||
@@ -35,14 +35,12 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
public final short subType; // из header
|
public final short subType;
|
||||||
public final short version; // из header
|
public final short version;
|
||||||
|
|
||||||
// line
|
public final int prevProfileBlockNumber;
|
||||||
public final int lineCode;
|
public final byte[] prevProfileBlockHash32;
|
||||||
public final int prevLineNumber;
|
public final int profileSequence;
|
||||||
public final byte[] prevLineHash32;
|
|
||||||
public final int thisLineNumber;
|
|
||||||
|
|
||||||
public final String paramKey;
|
public final String paramKey;
|
||||||
public final String paramValue;
|
public final String paramValue;
|
||||||
@@ -60,21 +58,17 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
// минимум: lineCode(4)+line(4+32+4) + keyLen(2)+key(1) + valLen(2)+val(1)
|
// prevGlobal(4)+prevHash(32)+seq(4) + keyLen(2)+key(1) + valLen(2)+val(1)
|
||||||
if (bodyBytes.length < 4 + (4 + 32 + 4) + 2 + 1 + 2 + 1) {
|
if (bodyBytes.length < 4 + 32 + 4 + 2 + 1 + 2 + 1) {
|
||||||
throw new IllegalArgumentException("UserParamBody too short");
|
throw new IllegalArgumentException("UserParamBody too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
this.lineCode = bb.getInt();
|
this.prevProfileBlockNumber = bb.getInt();
|
||||||
|
this.prevProfileBlockHash32 = new byte[32];
|
||||||
this.prevLineNumber = bb.getInt();
|
bb.get(this.prevProfileBlockHash32);
|
||||||
|
this.profileSequence = bb.getInt();
|
||||||
this.prevLineHash32 = new byte[32];
|
|
||||||
bb.get(this.prevLineHash32);
|
|
||||||
|
|
||||||
this.thisLineNumber = bb.getInt();
|
|
||||||
|
|
||||||
int keyLen = Short.toUnsignedInt(bb.getShort());
|
int keyLen = Short.toUnsignedInt(bb.getShort());
|
||||||
if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
|
if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
|
||||||
@@ -99,25 +93,21 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserParamBody(int lineCode,
|
public UserParamBody(int prevProfileBlockNumber,
|
||||||
int prevLineNumber,
|
byte[] prevProfileBlockHash32,
|
||||||
byte[] prevLineHash32,
|
int profileSequence,
|
||||||
int thisLineNumber,
|
|
||||||
String paramKey,
|
String paramKey,
|
||||||
String paramValue) {
|
String paramValue) {
|
||||||
|
|
||||||
Objects.requireNonNull(paramKey, "paramKey == null");
|
Objects.requireNonNull(paramKey, "paramKey == null");
|
||||||
Objects.requireNonNull(paramValue, "paramValue == null");
|
Objects.requireNonNull(paramValue, "paramValue == null");
|
||||||
|
|
||||||
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
|
||||||
|
|
||||||
this.subType = MsgSubType.USER_PARAM_TEXT_TEXT;
|
this.subType = MsgSubType.USER_PARAM_TEXT_TEXT;
|
||||||
this.version = VER;
|
this.version = VER;
|
||||||
|
|
||||||
this.lineCode = lineCode;
|
this.prevProfileBlockNumber = prevProfileBlockNumber;
|
||||||
this.prevLineNumber = prevLineNumber;
|
this.prevProfileBlockHash32 = (prevProfileBlockHash32 == null ? new byte[32] : Arrays.copyOf(prevProfileBlockHash32, 32));
|
||||||
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
this.profileSequence = profileSequence;
|
||||||
this.thisLineNumber = thisLineNumber;
|
|
||||||
|
|
||||||
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||||
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
@@ -128,17 +118,12 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserParamBody check() {
|
public UserParamBody check() {
|
||||||
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF)) {
|
||||||
|
|
||||||
if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF))
|
|
||||||
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
||||||
|
|
||||||
if (prevLineNumber == -1) {
|
|
||||||
if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
|
|
||||||
if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
|
|
||||||
} else {
|
|
||||||
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
|
||||||
}
|
}
|
||||||
|
if (prevProfileBlockNumber < 0) throw new IllegalArgumentException("prevProfileBlockNumber must be >=0");
|
||||||
|
if (prevProfileBlockHash32 == null || prevProfileBlockHash32.length != 32) throw new IllegalArgumentException("prevProfileBlockHash32 invalid");
|
||||||
|
if (profileSequence <= 0) throw new IllegalArgumentException("profileSequence must be >=1");
|
||||||
|
|
||||||
if (paramKey == null || paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
if (paramKey == null || paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||||
if (paramValue == null || paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
if (paramValue == null || paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||||
@@ -148,30 +133,25 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes() {
|
public byte[] toBytes() {
|
||||||
|
check();
|
||||||
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535");
|
if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535");
|
||||||
if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535");
|
if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535");
|
||||||
|
|
||||||
int cap = 4 + (4 + 32 + 4)
|
int cap = 4 + 32 + 4
|
||||||
+ 2 + keyUtf8.length
|
+ 2 + keyUtf8.length
|
||||||
+ 2 + valUtf8.length;
|
+ 2 + valUtf8.length;
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.putInt(prevProfileBlockNumber);
|
||||||
bb.putInt(lineCode);
|
bb.put(prevProfileBlockHash32);
|
||||||
|
bb.putInt(profileSequence);
|
||||||
bb.putInt(prevLineNumber);
|
|
||||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
|
||||||
bb.putInt(thisLineNumber);
|
|
||||||
|
|
||||||
bb.putShort((short) keyUtf8.length);
|
bb.putShort((short) keyUtf8.length);
|
||||||
bb.put(keyUtf8);
|
bb.put(keyUtf8);
|
||||||
|
|
||||||
bb.putShort((short) valUtf8.length);
|
bb.putShort((short) valUtf8.length);
|
||||||
bb.put(valUtf8);
|
bb.put(valUtf8);
|
||||||
|
|
||||||
return bb.array();
|
return bb.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,15 +167,9 @@ public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAllZero32(byte[] b) {
|
/* USER_PROFILE имеет единственную линию, логический lineCode всегда 0. */
|
||||||
if (b == null || b.length != 32) return true;
|
@Override public int lineCode() { return 0; }
|
||||||
for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
|
@Override public int prevLineBlockGlobalNumber() { return prevProfileBlockNumber; }
|
||||||
return true;
|
@Override public byte[] prevLineBlockHash32() { return Arrays.copyOf(prevProfileBlockHash32, 32); }
|
||||||
}
|
@Override public int lineSeq() { return profileSequence; }
|
||||||
|
|
||||||
/* ====================== BodyHasLine ====================== */
|
|
||||||
@Override public int lineCode() { return lineCode; }
|
|
||||||
@Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
|
|
||||||
@Override public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
|
||||||
@Override public int lineSeq() { return thisLineNumber; }
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,10 @@ final class ForkBodyTest {
|
|||||||
byte[] tipHash = filled(32, 0x33);
|
byte[] tipHash = filled(32, 0x33);
|
||||||
|
|
||||||
ForkBody source = new ForkBody(
|
ForkBody source = new ForkBody(
|
||||||
|
0,
|
||||||
|
77,
|
||||||
|
filled(32, 0x44),
|
||||||
|
5,
|
||||||
parentKey,
|
parentKey,
|
||||||
120,
|
120,
|
||||||
forkHash,
|
forkHash,
|
||||||
@@ -33,6 +37,9 @@ final class ForkBodyTest {
|
|||||||
BodyRecordParser.parse(ForkBody.TYPE, ForkBody.SUBTYPE, ForkBody.VER, source.toBytes())
|
BodyRecordParser.parse(ForkBody.TYPE, ForkBody.SUBTYPE, ForkBody.VER, source.toBytes())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assertEquals(0, parsed.lineCode);
|
||||||
|
assertEquals(77, parsed.prevLineNumber);
|
||||||
|
assertEquals(5, parsed.thisLineNumber);
|
||||||
assertArrayEquals(parentKey, parsed.parentBlockchainKey32);
|
assertArrayEquals(parentKey, parsed.parentBlockchainKey32);
|
||||||
assertEquals(120, parsed.forkPointBlockNumber);
|
assertEquals(120, parsed.forkPointBlockNumber);
|
||||||
assertArrayEquals(forkHash, parsed.forkPointBlockHash32);
|
assertArrayEquals(forkHash, parsed.forkPointBlockHash32);
|
||||||
@@ -46,6 +53,10 @@ final class ForkBodyTest {
|
|||||||
@Test
|
@Test
|
||||||
void routineRotationMayDiscardNothing() {
|
void routineRotationMayDiscardNothing() {
|
||||||
ForkBody body = new ForkBody(
|
ForkBody body = new ForkBody(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
filled(32, 9),
|
||||||
|
1,
|
||||||
filled(32, 1),
|
filled(32, 1),
|
||||||
10,
|
10,
|
||||||
filled(32, 2),
|
filled(32, 2),
|
||||||
@@ -63,6 +74,10 @@ final class ForkBodyTest {
|
|||||||
@Test
|
@Test
|
||||||
void rejectedWhenDiscardCountDoesNotMatchForkPointAndTip() {
|
void rejectedWhenDiscardCountDoesNotMatchForkPointAndTip() {
|
||||||
ForkBody body = new ForkBody(
|
ForkBody body = new ForkBody(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
filled(32, 9),
|
||||||
|
1,
|
||||||
filled(32, 1),
|
filled(32, 1),
|
||||||
10,
|
10,
|
||||||
filled(32, 2),
|
filled(32, 2),
|
||||||
|
|||||||
+5
-11
@@ -59,7 +59,7 @@ final class TargetLoginFormatTest {
|
|||||||
assertEquals(5, parsedEditPost.toBlockGlobalNumber());
|
assertEquals(5, parsedEditPost.toBlockGlobalNumber());
|
||||||
|
|
||||||
ConnectionBody connection = new ConnectionBody(
|
ConnectionBody connection = new ConnectionBody(
|
||||||
0, -1, new byte[32], -1,
|
0, HASH, 1,
|
||||||
MsgSubType.CONNECTION_FOLLOW,
|
MsgSubType.CONNECTION_FOLLOW,
|
||||||
"alice", 3, 0, HASH
|
"alice", 3, 0, HASH
|
||||||
);
|
);
|
||||||
@@ -84,16 +84,10 @@ final class TargetLoginFormatTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forkZeroIsRejectedForNewProtocol() {
|
void forkZeroIsReservedButStillDecodableAsHistoricalUnknown() {
|
||||||
assertThrows(IllegalArgumentException.class,
|
ReactionBody source = new ReactionBody(MsgSubType.REACTION_LIKE, "alice", 0, 42, HASH);
|
||||||
() -> new ReactionBody(MsgSubType.REACTION_LIKE, "alice", 0, 42, HASH));
|
ReactionBody parsed = new ReactionBody(MsgSubType.REACTION_LIKE, ReactionBody.VER, source.toBytes()).check();
|
||||||
|
assertEquals(0, parsed.toForkNumber());
|
||||||
byte[] encoded = new ReactionBody(MsgSubType.REACTION_LIKE, "alice", 1, 42, HASH).toBytes();
|
|
||||||
int forkOffset = 1 + "alice".getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
|
|
||||||
encoded[forkOffset] = 0;
|
|
||||||
encoded[forkOffset + 1] = 0;
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> new ReactionBody(MsgSubType.REACTION_LIKE, ReactionBody.VER, encoded));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -122,6 +122,19 @@ public final class BlocksDAO {
|
|||||||
}
|
}
|
||||||
public BlockEntry getByNumber(String bchName,int blockNumber)throws SQLException{try(Connection c=db.getConnection()){return getByNumber(c,bchName,blockNumber);} }
|
public BlockEntry getByNumber(String bchName,int blockNumber)throws SQLException{try(Connection c=db.getConnection()){return getByNumber(c,bchName,blockNumber);} }
|
||||||
|
|
||||||
|
/** Последний принятый блок конкретной логической линии в текущем physical fork. */
|
||||||
|
public BlockEntry getLastLineBlock(Connection c, String bchName, int msgType, int lineCode) throws SQLException {
|
||||||
|
String sql = baseSelect()+" WHERE login=? AND fork_number=? AND msg_type=? AND line_code=? ORDER BY block_number DESC LIMIT 1";
|
||||||
|
ChainRef r=chainRef(bchName);
|
||||||
|
try(PreparedStatement ps=c.prepareStatement(sql)){
|
||||||
|
ps.setString(1,r.login()); ps.setInt(2,r.fork()); ps.setInt(3,msgType); ps.setInt(4,lineCode);
|
||||||
|
try(ResultSet rs=ps.executeQuery()){ return rs.next()?mapRow(rs):null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public BlockEntry getLastLineBlock(String bchName, int msgType, int lineCode) throws SQLException {
|
||||||
|
try(Connection c=db.getConnection()){ return getLastLineBlock(c,bchName,msgType,lineCode); }
|
||||||
|
}
|
||||||
|
|
||||||
public List<BlockEntry> listRangeByNumber(String bchName,int from,int to)throws SQLException{try(Connection c=db.getConnection()){return listRangeByNumber(c,bchName,from,to);} }
|
public List<BlockEntry> listRangeByNumber(String bchName,int from,int to)throws SQLException{try(Connection c=db.getConnection()){return listRangeByNumber(c,bchName,from,to);} }
|
||||||
public List<BlockEntry> listRangeByNumber(Connection c,String bchName,int from,int to)throws SQLException{
|
public List<BlockEntry> listRangeByNumber(Connection c,String bchName,int from,int to)throws SQLException{
|
||||||
String sql=baseSelect()+" WHERE login=? AND fork_number=? AND block_number>=? AND block_number<=? ORDER BY block_number ASC";
|
String sql=baseSelect()+" WHERE login=? AND fork_number=? AND block_number>=? AND block_number<=? ORDER BY block_number ASC";
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ CREATE TABLE IF NOT EXISTS blocks (
|
|||||||
block_bytes BYTEA NOT NULL,
|
block_bytes BYTEA NOT NULL,
|
||||||
to_login TEXT,
|
to_login TEXT,
|
||||||
to_bch_name TEXT,
|
to_bch_name TEXT,
|
||||||
to_fork_number INTEGER CHECK (to_fork_number IS NULL OR (to_fork_number >= 1 AND to_fork_number <= 999)),
|
to_fork_number INTEGER CHECK (to_fork_number IS NULL OR (to_fork_number >= 0 AND to_fork_number <= 999)),
|
||||||
to_block_number INTEGER CHECK (to_block_number IS NULL OR to_block_number >= 0),
|
to_block_number INTEGER CHECK (to_block_number IS NULL OR to_block_number >= 0),
|
||||||
to_block_hash BYTEA,
|
to_block_hash BYTEA,
|
||||||
block_hash BYTEA NOT NULL,
|
block_hash BYTEA NOT NULL,
|
||||||
@@ -1110,6 +1110,7 @@ LANGUAGE plpgsql
|
|||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
prev_block RECORD;
|
prev_block RECORD;
|
||||||
|
tail_block_number INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.line_code IS NULL
|
IF NEW.line_code IS NULL
|
||||||
AND NEW.prev_line_number IS NULL
|
AND NEW.prev_line_number IS NULL
|
||||||
@@ -1149,26 +1150,35 @@ BEGIN
|
|||||||
RAISE EXCEPTION 'LINE_ERR_LINE_CODE_MISMATCH';
|
RAISE EXCEPTION 'LINE_ERR_LINE_CODE_MISMATCH';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF NEW.prev_line_number = NEW.line_code THEN
|
SELECT block_number
|
||||||
|
INTO tail_block_number
|
||||||
|
FROM blocks
|
||||||
|
WHERE bch_name = NEW.bch_name
|
||||||
|
AND msg_type = NEW.msg_type
|
||||||
|
AND line_code = NEW.line_code
|
||||||
|
ORDER BY block_number DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF tail_block_number IS NULL THEN
|
||||||
|
IF NEW.prev_line_number <> NEW.line_code THEN
|
||||||
|
RAISE EXCEPTION 'LINE_ERR_NOT_CURRENT_TAIL';
|
||||||
|
END IF;
|
||||||
IF NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END) THEN
|
IF NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END) THEN
|
||||||
RAISE EXCEPTION 'LINE_ERR_FIRST_STEP_BAD_THIS';
|
RAISE EXCEPTION 'LINE_ERR_FIRST_STEP_BAD_THIS';
|
||||||
END IF;
|
END IF;
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.prev_line_number <> tail_block_number THEN
|
||||||
|
RAISE EXCEPTION 'LINE_ERR_NOT_CURRENT_TAIL';
|
||||||
|
END IF;
|
||||||
|
|
||||||
IF prev_block.this_line_number IS NULL THEN
|
IF prev_block.this_line_number IS NULL THEN
|
||||||
RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP';
|
RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF NEW.msg_type = 1 THEN
|
IF NEW.this_line_number <> prev_block.this_line_number + 1 THEN
|
||||||
IF NEW.this_line_number <> prev_block.this_line_number
|
RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP';
|
||||||
AND NEW.this_line_number <> prev_block.this_line_number + 1 THEN
|
|
||||||
RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP';
|
|
||||||
END IF;
|
|
||||||
ELSE
|
|
||||||
IF NEW.this_line_number <> prev_block.this_line_number + 1 THEN
|
|
||||||
RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
@@ -2436,7 +2446,7 @@ $$;
|
|||||||
|
|
||||||
-- Physical chain integrity uses numeric fork identity.
|
-- Physical chain integrity uses numeric fork identity.
|
||||||
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi() RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi() RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||||
DECLARE prev_block RECORD;
|
DECLARE prev_block RECORD; tail_block_number INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.line_code IS NULL AND NEW.prev_line_number IS NULL AND NEW.prev_line_hash IS NULL AND NEW.this_line_number IS NULL THEN RETURN NEW; END IF;
|
IF NEW.line_code IS NULL AND NEW.prev_line_number IS NULL AND NEW.prev_line_hash IS NULL AND NEW.this_line_number IS NULL THEN RETURN NEW; END IF;
|
||||||
IF NEW.msg_type NOT IN (0,1,3,4) THEN RAISE EXCEPTION 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE'; END IF;
|
IF NEW.msg_type NOT IN (0,1,3,4) THEN RAISE EXCEPTION 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE'; END IF;
|
||||||
@@ -2445,15 +2455,17 @@ BEGIN
|
|||||||
IF NOT FOUND THEN RAISE EXCEPTION 'LINE_ERR_NO_PREV'; END IF;
|
IF NOT FOUND THEN RAISE EXCEPTION 'LINE_ERR_NO_PREV'; END IF;
|
||||||
IF prev_block.block_hash IS DISTINCT FROM NEW.prev_line_hash THEN RAISE EXCEPTION 'LINE_ERR_PREV_HASH_MISMATCH'; END IF;
|
IF prev_block.block_hash IS DISTINCT FROM NEW.prev_line_hash THEN RAISE EXCEPTION 'LINE_ERR_PREV_HASH_MISMATCH'; END IF;
|
||||||
IF NEW.prev_line_number<>NEW.line_code AND prev_block.line_code IS DISTINCT FROM NEW.line_code THEN RAISE EXCEPTION 'LINE_ERR_LINE_CODE_MISMATCH'; END IF;
|
IF NEW.prev_line_number<>NEW.line_code AND prev_block.line_code IS DISTINCT FROM NEW.line_code THEN RAISE EXCEPTION 'LINE_ERR_LINE_CODE_MISMATCH'; END IF;
|
||||||
IF NEW.prev_line_number=NEW.line_code THEN
|
SELECT block_number INTO tail_block_number FROM blocks_store
|
||||||
IF NEW.this_line_number<>(CASE WHEN NEW.msg_type=1 THEN 0 ELSE 1 END) THEN RAISE EXCEPTION 'LINE_ERR_FIRST_STEP_BAD_THIS'; END IF; RETURN NEW;
|
WHERE login=NEW.login AND fork_number=NEW.fork_number AND msg_type=NEW.msg_type AND line_code=NEW.line_code
|
||||||
|
ORDER BY block_number DESC LIMIT 1;
|
||||||
|
IF tail_block_number IS NULL THEN
|
||||||
|
IF NEW.prev_line_number<>NEW.line_code THEN RAISE EXCEPTION 'LINE_ERR_NOT_CURRENT_TAIL'; END IF;
|
||||||
|
IF NEW.this_line_number<>(CASE WHEN NEW.msg_type=1 THEN 0 ELSE 1 END) THEN RAISE EXCEPTION 'LINE_ERR_FIRST_STEP_BAD_THIS'; END IF;
|
||||||
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
IF NEW.prev_line_number<>tail_block_number THEN RAISE EXCEPTION 'LINE_ERR_NOT_CURRENT_TAIL'; END IF;
|
||||||
IF prev_block.this_line_number IS NULL THEN RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP'; END IF;
|
IF prev_block.this_line_number IS NULL THEN RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP'; END IF;
|
||||||
IF NEW.msg_type=1 THEN
|
IF NEW.this_line_number<>prev_block.this_line_number+1 THEN RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP'; END IF;
|
||||||
IF NEW.this_line_number<>prev_block.this_line_number AND NEW.this_line_number<>prev_block.this_line_number+1 THEN RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP'; END IF;
|
|
||||||
ELSE
|
|
||||||
IF NEW.this_line_number<>prev_block.this_line_number+1 THEN RAISE EXCEPTION 'LINE_ERR_THIS_LINE_BAD_STEP'; END IF;
|
|
||||||
END IF;
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
|||||||
+112
-29
@@ -8,6 +8,7 @@ import blockchain.body.BodyHasTarget;
|
|||||||
import blockchain.body.ConnectionBody;
|
import blockchain.body.ConnectionBody;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
import blockchain.body.HeaderBody;
|
import blockchain.body.HeaderBody;
|
||||||
|
import blockchain.body.ForkBody;
|
||||||
import blockchain.body.StatusActionBody;
|
import blockchain.body.StatusActionBody;
|
||||||
import blockchain.body.TextReplyBody;
|
import blockchain.body.TextReplyBody;
|
||||||
import blockchain.body.TextLineBody;
|
import blockchain.body.TextLineBody;
|
||||||
@@ -195,6 +196,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
case "limit_check_failed" -> "Ошибка проверки лимита размера";
|
case "limit_check_failed" -> "Ошибка проверки лимита размера";
|
||||||
case "bad_block_format" -> "Некорректный формат блока";
|
case "bad_block_format" -> "Некорректный формат блока";
|
||||||
case "bad_app_tag" -> "Некорректный тестовый тег ANS-104";
|
case "bad_app_tag" -> "Некорректный тестовый тег ANS-104";
|
||||||
|
case "bad_type_tag" -> "Некорректный индексный t-тег ANS-104";
|
||||||
case "bad_channel_tag" -> "Некорректный тег канала ANS-104";
|
case "bad_channel_tag" -> "Некорректный тег канала ANS-104";
|
||||||
case "bad_block_body" -> "Некорректное тело блока";
|
case "bad_block_body" -> "Некорректное тело блока";
|
||||||
case "bad_channel_name" -> "Некорректное название канала";
|
case "bad_channel_name" -> "Некорректное название канала";
|
||||||
@@ -204,9 +206,15 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
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" -> "Некорректная подпись блока";
|
||||||
case "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии";
|
case "prev_line_block_not_found", "line_root_not_found" -> "Не найден root/prev блок логической линии";
|
||||||
|
case "bad_line_code" -> "Некорректный код логической линии";
|
||||||
|
case "bad_line_root" -> "Некорректный root логической линии";
|
||||||
|
case "bad_line_state" -> "Повреждено состояние логической линии";
|
||||||
|
case "bad_prev_line_number" -> "Блок не продолжает текущий хвост логической линии";
|
||||||
case "bad_prev_line_hash" -> "Некорректный prevLineHash";
|
case "bad_prev_line_hash" -> "Некорректный prevLineHash";
|
||||||
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
case "bad_line_sequence" -> "Некорректный порядковый номер логической линии";
|
||||||
|
case "unsupported_line_body" -> "Этот body не поддерживает логическую линию";
|
||||||
|
case "db_error_prev_line_check", "db_error_line_check" -> "Ошибка БД при проверке логической линии";
|
||||||
case "channel_name_already_exists" -> "Такое название канала уже занято";
|
case "channel_name_already_exists" -> "Такое название канала уже занято";
|
||||||
case "repost_disabled" -> "Репосты временно отключены до будущей реализации";
|
case "repost_disabled" -> "Репосты временно отключены до будущей реализации";
|
||||||
case "entrypoint_edit_forbidden" -> "TEXT_ENTRYPOINT нельзя редактировать через TEXT_EDIT_POST";
|
case "entrypoint_edit_forbidden" -> "TEXT_ENTRYPOINT нельзя редактировать через TEXT_EDIT_POST";
|
||||||
@@ -312,6 +320,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
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.
|
||||||
|
String expectedTypeTag = expectedTypeTag(block);
|
||||||
|
String actualTypeTag = block.getDataItem().tagValue("t");
|
||||||
|
if (expectedTypeTag != null) {
|
||||||
|
if (!expectedTypeTag.equals(actualTypeTag)) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_type_tag", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
} else if (actualTypeTag != null) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_type_tag", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
|
||||||
// body.check()
|
// body.check()
|
||||||
try {
|
try {
|
||||||
block.body.check();
|
block.body.check();
|
||||||
@@ -540,6 +559,11 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverLastNum == -1 && block.body instanceof HeaderBody headerBody
|
||||||
|
&& !Arrays.equals(headerBody.initialBlockchainKey32, pubKey32)) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "header_initial_key_mismatch", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
|
||||||
// 6) ANS-104 Ed25519 подпись и owner
|
// 6) ANS-104 Ed25519 подпись и owner
|
||||||
boolean sigOk;
|
boolean sigOk;
|
||||||
try {
|
try {
|
||||||
@@ -556,7 +580,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) line columns (only for BodyHasLine)
|
// 7) line columns + strict continuity check.
|
||||||
Integer lineCode = null;
|
Integer lineCode = null;
|
||||||
Integer prevLineNumber = null;
|
Integer prevLineNumber = null;
|
||||||
byte[] prevLineHash32 = null;
|
byte[] prevLineHash32 = null;
|
||||||
@@ -568,33 +592,17 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
prevLineHash32 = bl.prevLineBlockHash32();
|
prevLineHash32 = bl.prevLineBlockHash32();
|
||||||
thisLineNumber = bl.lineSeq();
|
thisLineNumber = bl.lineSeq();
|
||||||
|
|
||||||
// Нормализация: пустой line anchor (-1) не пишем в DB projection
|
try {
|
||||||
if (prevLineNumber != null && prevLineNumber == -1) {
|
String lineError = validateLineContinuity(blockchainName, block, bl);
|
||||||
lineCode = null;
|
if (lineError != null) {
|
||||||
prevLineNumber = null;
|
log.warn("AddBlock: {} (login={}, blockchainName={}, blockNumber={}, lineCode={}, prevLineNumber={}, lineSeq={})",
|
||||||
prevLineHash32 = null;
|
lineError, login, blockchainName, block.blockNumber, lineCode, prevLineNumber, thisLineNumber);
|
||||||
thisLineNumber = null;
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, lineError, serverLastNum, serverLastHashHex);
|
||||||
}
|
|
||||||
|
|
||||||
// Если prevLineNumber задан — проверяем его хэш
|
|
||||||
if (prevLineNumber != null) {
|
|
||||||
try {
|
|
||||||
byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
|
|
||||||
if (dbPrevHash == null) {
|
|
||||||
log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
|
||||||
login, blockchainName, block.blockNumber, prevLineNumber);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
|
|
||||||
log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
|
|
||||||
login, blockchainName, block.blockNumber, prevLineNumber);
|
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
|
|
||||||
login, blockchainName, block.blockNumber, e);
|
|
||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AddBlock: db_error_line_check (login={}, blockchainName={}, blockNumber={})",
|
||||||
|
login, blockchainName, block.blockNumber, e);
|
||||||
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_line_check", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,6 +763,81 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
/* ====================== Helpers ====================================== */
|
/* ====================== Helpers ====================================== */
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
private static String expectedTypeTag(BchBlockEntry block) {
|
||||||
|
int type = block.type & 0xffff;
|
||||||
|
return switch (type) {
|
||||||
|
case 0 -> "tech";
|
||||||
|
case 3 -> "conn";
|
||||||
|
case 4 -> "prof";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет не просто существование prev-блока, а именно продолжение текущего хвоста линии.
|
||||||
|
* Это запрещает боковые ветки линии и гарантирует монотонный sequence.
|
||||||
|
*/
|
||||||
|
private String validateLineContinuity(String blockchainName, BchBlockEntry block, BodyHasLine line) throws Exception {
|
||||||
|
int type = block.type & 0xffff;
|
||||||
|
int lineCode = line.lineCode();
|
||||||
|
int rootBlockNumber;
|
||||||
|
int firstSequence;
|
||||||
|
|
||||||
|
if (block.body instanceof UserParamBody || block.body instanceof ConnectionBody) {
|
||||||
|
if (lineCode != 0) return "bad_line_code";
|
||||||
|
rootBlockNumber = 0;
|
||||||
|
firstSequence = 1;
|
||||||
|
} else if (block.body instanceof CreateChannelBody || block.body instanceof ForkBody) {
|
||||||
|
if (lineCode != 0) return "bad_line_code";
|
||||||
|
rootBlockNumber = 0;
|
||||||
|
firstSequence = 1;
|
||||||
|
} else if (block.body instanceof TextLineBody) {
|
||||||
|
if (lineCode < 0) return "bad_line_code";
|
||||||
|
rootBlockNumber = lineCode;
|
||||||
|
// Channel sequence is zero-based after its root anchor.
|
||||||
|
firstSequence = 0;
|
||||||
|
} else {
|
||||||
|
return "unsupported_line_body";
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockEntry root = blocksDAO.getByNumber(blockchainName, rootBlockNumber);
|
||||||
|
if (root == null) return "line_root_not_found";
|
||||||
|
if (rootBlockNumber == 0) {
|
||||||
|
try {
|
||||||
|
if (!(new BchBlockEntry(root.getBlockBytes()).body instanceof HeaderBody)) return "bad_line_root";
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "bad_line_root";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (!(new BchBlockEntry(root.getBlockBytes()).body instanceof CreateChannelBody)) return "bad_line_root";
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "bad_line_root";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockEntry tail = blocksDAO.getLastLineBlock(blockchainName, type, lineCode);
|
||||||
|
int expectedPrevNumber;
|
||||||
|
byte[] expectedPrevHash;
|
||||||
|
int expectedSequence;
|
||||||
|
if (tail == null) {
|
||||||
|
expectedPrevNumber = rootBlockNumber;
|
||||||
|
expectedPrevHash = root.getBlockHash();
|
||||||
|
expectedSequence = firstSequence;
|
||||||
|
} else {
|
||||||
|
if (tail.getThisLineNumber() == null) return "bad_line_state";
|
||||||
|
expectedPrevNumber = tail.getBlockNumber();
|
||||||
|
expectedPrevHash = tail.getBlockHash();
|
||||||
|
expectedSequence = Math.addExact(tail.getThisLineNumber(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.prevLineBlockGlobalNumber() != expectedPrevNumber) return "bad_prev_line_number";
|
||||||
|
byte[] suppliedPrevHash = require32OrThrow(line.prevLineBlockHash32(), "prevLineHash32 invalid");
|
||||||
|
if (!Arrays.equals(expectedPrevHash, suppliedPrevHash)) return "bad_prev_line_hash";
|
||||||
|
if (line.lineSeq() != expectedSequence) return "bad_line_sequence";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String expectedChannelSlug(String blockchainName, BchBlockEntry block, ChannelNameStateEntry justCreated) throws Exception {
|
private String expectedChannelSlug(String blockchainName, BchBlockEntry block, ChannelNameStateEntry justCreated) throws Exception {
|
||||||
if (block.body instanceof CreateChannelBody) {
|
if (block.body instanceof CreateChannelBody) {
|
||||||
return justCreated == null ? null : justCreated.getSlug();
|
return justCreated == null ? null : justCreated.getSlug();
|
||||||
|
|||||||
+15
@@ -101,6 +101,11 @@ public final class Net_KeyRotationAddBlock_Handler implements JsonMessageHandler
|
|||||||
if (!block.getDataItem().hasTag("App", "test5590")) {
|
if (!block.getDataItem().hasTag("App", "test5590")) {
|
||||||
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_APP_TAG", "Отсутствует App=test5590");
|
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_APP_TAG", "Отсутствует App=test5590");
|
||||||
}
|
}
|
||||||
|
String expectedTypeTag = expectedTypeTag(block);
|
||||||
|
String actualTypeTag = block.getDataItem().tagValue("t");
|
||||||
|
if (expectedTypeTag != null ? !expectedTypeTag.equals(actualTypeTag) : actualTypeTag != null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 400, "KEY_ROTATION_BAD_TYPE_TAG", "Некорректный t-тег candidate DataItem");
|
||||||
|
}
|
||||||
if (!requestPrevHashMatches(req.getPrevBlockHash(), block.prevHash32)) {
|
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 запроса не совпадает с блоком");
|
||||||
}
|
}
|
||||||
@@ -221,4 +226,14 @@ public final class Net_KeyRotationAddBlock_Handler implements JsonMessageHandler
|
|||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
return value.trim().replace("\r\n", "\n").replace('\r', '\n');
|
return value.trim().replace("\r\n", "\n").replace('\r', '\n');
|
||||||
}
|
}
|
||||||
|
private static String expectedTypeTag(BchBlockEntry block) {
|
||||||
|
int type = block.type & 0xffff;
|
||||||
|
return switch (type) {
|
||||||
|
case 0 -> "tech";
|
||||||
|
case 3 -> "conn";
|
||||||
|
case 4 -> "prof";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -148,6 +148,9 @@ public final class Net_KeyRotationFinishChain_Handler implements JsonMessageHand
|
|||||||
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("App", "test5590")) return "KEY_ROTATION_BAD_APP_TAG";
|
||||||
|
String expectedTypeTag = expectedTypeTag(candidate);
|
||||||
|
String actualTypeTag = candidate.getDataItem().tagValue("t");
|
||||||
|
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";
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
@@ -215,4 +218,14 @@ public final class Net_KeyRotationFinishChain_Handler implements JsonMessageHand
|
|||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
return value.trim().replace("\r\n", "\n").replace('\r', '\n');
|
return value.trim().replace("\r\n", "\n").replace('\r', '\n');
|
||||||
}
|
}
|
||||||
|
private static String expectedTypeTag(BchBlockEntry block) {
|
||||||
|
int type = block.type & 0xffff;
|
||||||
|
return switch (type) {
|
||||||
|
case 0 -> "tech";
|
||||||
|
case 3 -> "conn";
|
||||||
|
case 4 -> "prof";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ public final class AddBlockSender {
|
|||||||
|
|
||||||
List<Ans104DataItem.Tag> tags = new ArrayList<>();
|
List<Ans104DataItem.Tag> tags = new ArrayList<>();
|
||||||
tags.add(new Ans104DataItem.Tag("App", "test5590"));
|
tags.add(new Ans104DataItem.Tag("App", "test5590"));
|
||||||
|
String typeTag = typeTagFor(type);
|
||||||
|
if (typeTag != null) tags.add(new Ans104DataItem.Tag("t", 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("c_test5590", channelSlug));
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@ public final class AddBlockSender {
|
|||||||
private static short typeOf(BodyRecord body) {
|
private static short typeOf(BodyRecord body) {
|
||||||
if (body instanceof HeaderBody) return HeaderBody.TYPE;
|
if (body instanceof HeaderBody) return HeaderBody.TYPE;
|
||||||
if (body instanceof CreateChannelBody) return CreateChannelBody.TYPE;
|
if (body instanceof CreateChannelBody) return CreateChannelBody.TYPE;
|
||||||
|
if (body instanceof ForkBody) return ForkBody.TYPE;
|
||||||
if (body instanceof TextLineBody || body instanceof TextReplyBody || body instanceof TextRatingBody) return TextLineBody.TYPE;
|
if (body instanceof TextLineBody || body instanceof TextReplyBody || body instanceof TextRatingBody) return TextLineBody.TYPE;
|
||||||
if (body instanceof StatusActionBody) return StatusActionBody.TYPE;
|
if (body instanceof StatusActionBody) return StatusActionBody.TYPE;
|
||||||
if (body instanceof ReactionBody) return ReactionBody.TYPE;
|
if (body instanceof ReactionBody) return ReactionBody.TYPE;
|
||||||
@@ -178,6 +181,7 @@ public final class AddBlockSender {
|
|||||||
private static short subTypeOf(BodyRecord body) {
|
private static short subTypeOf(BodyRecord body) {
|
||||||
if (body instanceof HeaderBody hb) return hb.subType;
|
if (body instanceof HeaderBody hb) return hb.subType;
|
||||||
if (body instanceof CreateChannelBody cb) return cb.subType;
|
if (body instanceof CreateChannelBody cb) return cb.subType;
|
||||||
|
if (body instanceof ForkBody fb) return fb.subType;
|
||||||
if (body instanceof TextLineBody tb) return tb.subType;
|
if (body instanceof TextLineBody tb) return tb.subType;
|
||||||
if (body instanceof TextReplyBody tb) return tb.subType;
|
if (body instanceof TextReplyBody tb) return tb.subType;
|
||||||
if (body instanceof TextRatingBody tb) return tb.subType;
|
if (body instanceof TextRatingBody tb) return tb.subType;
|
||||||
@@ -191,6 +195,7 @@ public final class AddBlockSender {
|
|||||||
private static short versionOf(BodyRecord body) {
|
private static short versionOf(BodyRecord body) {
|
||||||
if (body instanceof HeaderBody hb) return hb.version;
|
if (body instanceof HeaderBody hb) return hb.version;
|
||||||
if (body instanceof CreateChannelBody cb) return cb.version;
|
if (body instanceof CreateChannelBody cb) return cb.version;
|
||||||
|
if (body instanceof ForkBody fb) return fb.version;
|
||||||
if (body instanceof TextLineBody tb) return tb.version;
|
if (body instanceof TextLineBody tb) return tb.version;
|
||||||
if (body instanceof TextReplyBody tb) return tb.version;
|
if (body instanceof TextReplyBody tb) return tb.version;
|
||||||
if (body instanceof TextRatingBody tb) return tb.version;
|
if (body instanceof TextRatingBody tb) return tb.version;
|
||||||
@@ -203,6 +208,15 @@ public final class AddBlockSender {
|
|||||||
|
|
||||||
// ---------- Frame v1 + signed channel tag helpers ----------
|
// ---------- Frame v1 + signed channel tag helpers ----------
|
||||||
|
|
||||||
|
private static String typeTagFor(short type) {
|
||||||
|
return switch (type & 0xFFFF) {
|
||||||
|
case 0 -> "tech";
|
||||||
|
case 3 -> "conn";
|
||||||
|
case 4 -> "prof";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private String channelSlugFor(BodyRecord body) {
|
private String channelSlugFor(BodyRecord body) {
|
||||||
if (body instanceof CreateChannelBody ccb) {
|
if (body instanceof CreateChannelBody ccb) {
|
||||||
return shine.db.channels.ChannelNameRules.toCanonicalSlug(ccb.channelName);
|
return shine.db.channels.ChannelNameRules.toCanonicalSlug(ccb.channelName);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package test.it.blockchain;
|
|||||||
import blockchain.body.BodyRecord;
|
import blockchain.body.BodyRecord;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
import blockchain.body.ForkBody;
|
||||||
import blockchain.body.TextLineBody;
|
import blockchain.body.TextLineBody;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -16,10 +17,10 @@ import java.util.Map;
|
|||||||
* - map blockNumber -> hash32
|
* - map blockNumber -> hash32
|
||||||
*
|
*
|
||||||
* Линии:
|
* Линии:
|
||||||
* - TECH (type=0): только CREATE_CHANNEL (hasLine), root = HEADER
|
* - TECH (type=0): CREATE_CHANNEL и TECH_FORK, root = HEADER
|
||||||
* - TEXT (type=1): линии каналов, root = HEADER (канал "0") или CREATE_CHANNEL (канал "X")
|
* - TEXT (type=1): линии каналов, root = HEADER (канал "0") или CREATE_CHANNEL (канал "X")
|
||||||
* - CONNECTION (type=3): одна линия
|
* - CONNECTION (type=3): одна линия
|
||||||
* - USER_PARAM (type=4): одна линия
|
* - USER_PROFILE (type=4): одна линия
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
* - prevLineNumber — это GLOBAL blockNumber предыдущего блока линии.
|
* - prevLineNumber — это GLOBAL blockNumber предыдущего блока линии.
|
||||||
@@ -277,16 +278,18 @@ public final class ChainState {
|
|||||||
|
|
||||||
int t = type & 0xFFFF;
|
int t = type & 0xFFFF;
|
||||||
|
|
||||||
// ---- TECH (CREATE_CHANNEL) ----
|
// ---- TECH (CREATE_CHANNEL / TECH_FORK) ----
|
||||||
if (t == TYPE_TECH && body instanceof CreateChannelBody ccb) {
|
if (t == TYPE_TECH && body instanceof BodyHasLine tech) {
|
||||||
techLine.lastGlobalNumber = blockNumber;
|
techLine.lastGlobalNumber = blockNumber;
|
||||||
techLine.lastHashHex = hex64;
|
techLine.lastHashHex = hex64;
|
||||||
techLine.lastThisLineNumber = ccb.thisLineNumber;
|
techLine.lastThisLineNumber = tech.lineSeq();
|
||||||
|
|
||||||
// ВАЖНО: CREATE_CHANNEL — это root нового текстового канала:
|
|
||||||
// lineCode для этого канала = blockNumber CREATE_CHANNEL
|
|
||||||
registerTextChannelRoot(blockNumber, hash32);
|
|
||||||
|
|
||||||
|
if (body instanceof CreateChannelBody) {
|
||||||
|
// CREATE_CHANNEL одновременно является root нового текстового канала.
|
||||||
|
registerTextChannelRoot(blockNumber, hash32);
|
||||||
|
} else if (!(body instanceof ForkBody)) {
|
||||||
|
throw new IllegalStateException("Unknown TECH line body: " + body.getClass());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -331,11 +331,7 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
|
|
||||||
var ln = st.nextLineByType(ChainState.TYPE_CONNECTION);
|
var ln = st.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||||
|
|
||||||
// КОНСТРУКТОР ИЗ ТВОЕГО КОДА:
|
|
||||||
// ConnectionBody(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber,
|
|
||||||
// short subType, String toLogin, int toBlockGlobalNumber, byte[] toBlockHash32)
|
|
||||||
sender.send(new ConnectionBody(
|
sender.send(new ConnectionBody(
|
||||||
0, // lineCode для connection линии
|
|
||||||
ln.prevLineNumber,
|
ln.prevLineNumber,
|
||||||
ln.prevLineHash32,
|
ln.prevLineHash32,
|
||||||
ln.thisLineNumber,
|
ln.thisLineNumber,
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ public final class SeedDataPopulationHelper {
|
|||||||
ChainState state = stateFor(login);
|
ChainState state = stateFor(login);
|
||||||
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_USER_PARAM);
|
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_USER_PARAM);
|
||||||
sender.send(new UserParamBody(
|
sender.send(new UserParamBody(
|
||||||
0,
|
|
||||||
line.prevLineNumber,
|
line.prevLineNumber,
|
||||||
line.prevLineHash32,
|
line.prevLineHash32,
|
||||||
line.thisLineNumber,
|
line.thisLineNumber,
|
||||||
@@ -148,7 +147,6 @@ public final class SeedDataPopulationHelper {
|
|||||||
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_CONNECTION);
|
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||||
|
|
||||||
sender.send(new ConnectionBody(
|
sender.send(new ConnectionBody(
|
||||||
0,
|
|
||||||
line.prevLineNumber,
|
line.prevLineNumber,
|
||||||
line.prevLineHash32,
|
line.prevLineHash32,
|
||||||
line.thisLineNumber,
|
line.thisLineNumber,
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.14.4
|
client.version=1.14.5
|
||||||
server.version=1.12.2
|
server.version=1.12.3
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- `11_TEXT_Blocks.md` — текстовые блоки (`type=1`).
|
- `11_TEXT_Blocks.md` — текстовые блоки (`type=1`).
|
||||||
- `12_REACTION_Blocks.md` — реакции (`type=2`).
|
- `12_REACTION_Blocks.md` — реакции (`type=2`).
|
||||||
- `13_CONNECTION_Blocks.md` — связи/подписки (`type=3`).
|
- `13_CONNECTION_Blocks.md` — связи/подписки (`type=3`).
|
||||||
- `14_USER_PARAM_Blocks.md` — пользовательские параметры (`type=4`).
|
- `14_USER_PROFILE_Blocks.md` — профиль пользователя (`type=4`).
|
||||||
- `15_STATUS_ACTION_Blocks.md` — статусные действия (`type=5`).
|
- `15_STATUS_ACTION_Blocks.md` — статусные действия (`type=5`).
|
||||||
|
|
||||||
## Быстрая карта типов
|
## Быстрая карта типов
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/RATING/REPOST/CHANNEL_META/ENTRYPOINT/EXERCISE/SERVICE/COURSE.
|
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/RATING/REPOST/CHANNEL_META/ENTRYPOINT/EXERCISE/SERVICE/COURSE.
|
||||||
- `type=2` — REACTION: LIKE/UNLIKE.
|
- `type=2` — REACTION: LIKE/UNLIKE.
|
||||||
- `type=3` — CONNECTION: FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING и обратные операции.
|
- `type=3` — CONNECTION: FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING и обратные операции.
|
||||||
- `type=4` — USER_PARAM: key/value-параметры пользователя.
|
- `type=4` — USER_PROFILE: история полей профиля пользователя.
|
||||||
- `type=5` — STATUS_ACTION: DONE_ONCE/LEARNED/SERVICE_PASSED/CONFIRMED/INTERESTED/STARTED/IN_STUDY/ABANDONED/COMPLETED.
|
- `type=5` — STATUS_ACTION: DONE_ONCE/LEARNED/SERVICE_PASSED/CONFIRMED/INTERESTED/STARTED/IN_STUDY/ABANDONED/COMPLETED.
|
||||||
|
|
||||||
## Примечание
|
## Примечание
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ App = test5590
|
|||||||
|
|
||||||
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
|
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
|
||||||
|
|
||||||
|
### Индексный тег фиксированного потока
|
||||||
|
|
||||||
|
Для трёх фиксированных потоков используется короткий подписанный тег:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=tech # TECH (HEADER / CREATE_CHANNEL / FORK)
|
||||||
|
t=conn # CONNECTION
|
||||||
|
t=prof # USER_PROFILE
|
||||||
|
```
|
||||||
|
|
||||||
|
Для TEXT, REACTION и STATUS_ACTION тег `t` отсутствует. Сервер проверяет точное соответствие значения `t` фактическому `msg_type`.
|
||||||
|
|
||||||
### Канальный тег
|
### Канальный тег
|
||||||
|
|
||||||
Если блок относится к конкретному каналу, он дополнительно содержит:
|
Если блок относится к конкретному каналу, он дополнительно содержит:
|
||||||
@@ -107,13 +119,15 @@ Slug входит в подпись DataItem и не может быть изм
|
|||||||
|
|
||||||
1. распарсить полный ANS-104 DataItem;
|
1. распарсить полный ANS-104 DataItem;
|
||||||
2. проверить `App=test5590`;
|
2. проверить `App=test5590`;
|
||||||
3. проверить `c_test5590`, если тип блока требует канал;
|
3. проверить `t`, если тип блока требует фиксированный индексный поток;
|
||||||
4. проверить ANS-104 Ed25519 подпись;
|
4. проверить `c_test5590`, если тип блока требует канал;
|
||||||
5. проверить, что `owner` равен текущему blockchain public key пользователя;
|
5. проверить ANS-104 Ed25519 подпись;
|
||||||
6. распарсить Frame v1 и body;
|
6. проверить, что `owner` равен текущему blockchain public key пользователя;
|
||||||
7. проверить `blockNumber == last + 1`;
|
7. распарсить Frame v1 и body;
|
||||||
8. проверить `prevHash32 == lastBlockHash`;
|
8. проверить `blockNumber == last + 1`;
|
||||||
9. записать DataItem и новое состояние атомарно в PostgreSQL.
|
9. проверить `prevHash32 == lastBlockHash`;
|
||||||
|
10. проверить строгую непрерывность логической линии, если body line-based;
|
||||||
|
11. записать DataItem и новое состояние атомарно в PostgreSQL.
|
||||||
|
|
||||||
|
|
||||||
## Нормативное правило версий body
|
## Нормативное правило версий body
|
||||||
|
|||||||
@@ -2,47 +2,87 @@
|
|||||||
|
|
||||||
## 1. Именованный блокчейн
|
## 1. Именованный блокчейн
|
||||||
|
|
||||||
Базовый идентификатор цепочки пользователя:
|
Базовый идентификатор физической цепочки пользователя:
|
||||||
|
|
||||||
- `blockchainName = <login>-<NNN>`
|
- `blockchainName = <login>-<NNN>`;
|
||||||
- `NNN` — реальный номер fork строго `001..999`; `000` и `1000+` недопустимы;
|
- `NNN` — номер fork `001..999`;
|
||||||
- пример: `alice-001`
|
- пример: `alice-001`.
|
||||||
|
|
||||||
Обычно это одна основная цепочка пользователя.
|
## 2. Логические линии внутри пользовательской цепочки
|
||||||
|
|
||||||
## 2. Логические линии внутри одной цепочки
|
Глобально все блоки пользователя идут одной последовательностью `blockNumber` и `prevHash32`. Дополнительно некоторые типы образуют собственные проверяемые линии.
|
||||||
|
|
||||||
Физически цепочка одна, но внутри есть независимые логические последовательности (линии), которые ведутся через поля:
|
Актуальные линии:
|
||||||
|
|
||||||
- `lineCode`
|
- `TECH` — системная история: `TECH_CREATE_CHANNEL`, `TECH_FORK`; root — `HEADER`;
|
||||||
- `prevLineNumber`
|
- `TEXT` — отдельная линия каждого канала; root — `HEADER` для канала `0` или `TECH_CREATE_CHANNEL` для пользовательского канала;
|
||||||
- `prevLineHash32`
|
- `CONNECTION` — единая история изменений связей пользователя; root — `HEADER`;
|
||||||
- `thisLineNumber`
|
- `USER_PROFILE` — единая история изменений профиля пользователя; root — `HEADER`.
|
||||||
|
|
||||||
Линии используются для:
|
`STATUS_ACTION` в line-модель сейчас не входит.
|
||||||
- TECH-событий;
|
|
||||||
- каналов с текстовыми постами;
|
|
||||||
- связей и подписок;
|
|
||||||
- пользовательских параметров.
|
|
||||||
|
|
||||||
## 3. Правила line-полей (фактическая серверная валидация)
|
## 3. Два формата line-prefix
|
||||||
|
|
||||||
Line-поля: `lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`.
|
### 3.1. Линии с явным `lineCode`
|
||||||
|
|
||||||
- Line-поля разрешены только для `msg_type`: `0`, `1`, `3`, `4`.
|
`TECH` и `TEXT` хранят:
|
||||||
- Если передано хотя бы одно line-поле, должны быть переданы все 4.
|
|
||||||
- `prevLineNumber/prevLineHash32` должны указывать на существующий блок этой же цепочки.
|
|
||||||
- Для первого шага после root (`prevLineNumber == lineCode`):
|
|
||||||
- `TEXT (msg_type=1)`: `thisLineNumber = 0`;
|
|
||||||
- `TECH/CONNECTION/USER_PARAM (0/3/4)`: `thisLineNumber = 1`.
|
|
||||||
- Для обычного шага:
|
|
||||||
- `TEXT`: `thisLineNumber` допускает `same` или `+1` от предыдущего блока линии;
|
|
||||||
- `TECH/CONNECTION/USER_PARAM`: строго `+1`.
|
|
||||||
|
|
||||||
## 4. Root-идея для каналов и подписок
|
```text
|
||||||
|
[4] lineCode
|
||||||
|
[4] prevLineNumber
|
||||||
|
[32] prevLineHash32
|
||||||
|
[4] thisLineNumber
|
||||||
|
```
|
||||||
|
|
||||||
Для ссылок вида follow/friend/contact принято ссылаться на корневые блоки:
|
- для TECH `lineCode=0`;
|
||||||
- `HEADER` для базовой сущности пользователя/канала `0`;
|
- для канала `lineCode = global blockNumber` его root-блока (`HEADER #0` либо `TECH_CREATE_CHANNEL`).
|
||||||
- `CREATE_CHANNEL` для пользовательских каналов.
|
|
||||||
|
|
||||||
Так ссылки остаются стабильными, даже когда в канале появляются новые сообщения.
|
### 3.2. Фиксированные линии
|
||||||
|
|
||||||
|
`CONNECTION` и `USER_PROFILE` имеют по одной линии на пользователя, поэтому `lineCode` в body не хранится. Их префикс:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[4] prevLineNumber
|
||||||
|
[32] prevLineHash32
|
||||||
|
[4] thisLineNumber
|
||||||
|
```
|
||||||
|
|
||||||
|
Логический `lineCode` этих линий всегда `0`.
|
||||||
|
|
||||||
|
## 4. Что означают номера
|
||||||
|
|
||||||
|
`prevLineNumber` — **глобальный `blockNumber` предыдущего блока этой линии**.
|
||||||
|
|
||||||
|
`thisLineNumber` — локальный порядковый номер внутри линии:
|
||||||
|
|
||||||
|
- первый TEXT-блок канала: `0`, далее `1,2,3...`;
|
||||||
|
- первый TECH после HEADER: `1`, далее `2,3,4...`;
|
||||||
|
- первый CONNECTION: `1`, далее `2,3,4...`;
|
||||||
|
- первый USER_PROFILE: `1`, далее `2,3,4...`.
|
||||||
|
|
||||||
|
То есть `prevLineNumber` и `thisLineNumber` — принципиально разные числа.
|
||||||
|
|
||||||
|
## 5. Строгая серверная валидация
|
||||||
|
|
||||||
|
Для каждого line-блока сервер определяет **текущий фактический хвост той же линии** и требует:
|
||||||
|
|
||||||
|
1. `prevLineNumber` равен глобальному номеру текущего хвоста (либо root для первого блока);
|
||||||
|
2. `prevLineHash32` равен hash этого хвоста/root;
|
||||||
|
3. `thisLineNumber` равен предыдущему sequence `+1` (для первого TEXT — `0`, для остальных линий — `1`);
|
||||||
|
4. root существует и имеет правильный тип.
|
||||||
|
|
||||||
|
Ссылка просто на любой существующий блок больше не считается допустимой. Это запрещает случайные или намеренные боковые ветки линии.
|
||||||
|
|
||||||
|
Для канала в одну линию входят все line-based события этого канала, включая обычные посты, `EDIT_POST`, `REPOST`, `CHANNEL_META` и контентные типы канала. Поэтому редактирование не создаёт отдельную боковую ветку.
|
||||||
|
|
||||||
|
## 6. Индексные ANS-104 теги
|
||||||
|
|
||||||
|
Фиксированные системные потоки дополнительно индексируются подписанным тегом:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=prof # USER_PROFILE
|
||||||
|
t=conn # CONNECTION
|
||||||
|
t=tech # TECH, включая HEADER / CREATE_CHANNEL / FORK
|
||||||
|
```
|
||||||
|
|
||||||
|
Тег является индексом для выборочной загрузки; криптографическая непрерывность обеспечивается line-полями внутри подписанного Frame/DataItem.
|
||||||
|
|||||||
@@ -1,26 +1,76 @@
|
|||||||
# TECH блоки (`type=0`, `version=1`)
|
# TECH блоки (`type=0`, `version=1`)
|
||||||
|
|
||||||
TECH-тип покрывает системные записи цепочки.
|
TECH-тип покрывает системные записи пользовательской цепочки. Все TECH DataItem имеют подписанный индексный тег:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=tech
|
||||||
|
```
|
||||||
|
|
||||||
|
`HEADER` является root TECH-линии. Следующие line-based TECH-события (`TECH_CREATE_CHANNEL`, `TECH_FORK`) образуют одну последовательность с `lineCode=0`, ссылкой на предыдущий TECH-событийный блок и sequence `1..N`.
|
||||||
|
|
||||||
## Подтипы
|
## Подтипы
|
||||||
|
|
||||||
1. `subType=0` — `HEADER_COMPAT`
|
### `subType=0` — `HEADER_COMPAT`
|
||||||
- стартовый блок цепочки;
|
|
||||||
- payload: tag `SHiNE` + login владельца + `initialBlockchainKey32`;
|
|
||||||
- `initialBlockchainKey32` — public blockchain-signing key, которым был создан fork №1; это историческая точка происхождения и при последующих fork сохраняется в скопированном HEADER байт-в-байт.
|
|
||||||
|
|
||||||
2. `subType=1` — `TECH_CREATE_CHANNEL`
|
Стартовый блок цепочки (`block #0`). Payload содержит tag `SHiNE`, login владельца и `initialBlockchainKey32`.
|
||||||
- создание нового канала;
|
|
||||||
- хранит line-поля + `channelName` + `channelDescription` + `channelType` + `channelTypeVersion`.
|
|
||||||
|
|
||||||
3. `subType=2` — `TECH_FORK`
|
`initialBlockchainKey32` — public blockchain-signing key, которым был создан fork №1; это историческая точка происхождения. При последующих fork значение не меняется.
|
||||||
- первый новый блок после точной перепубликации выбранного префикса предыдущего fork новым blockchain key;
|
|
||||||
- связывает новую активную цепочку с предыдущей и фиксирует точку rollback/продолжения.
|
|
||||||
|
|
||||||
### `TECH_FORK` body (`version=1`)
|
HEADER сам не хранит line-prefix, но является root TECH-линии.
|
||||||
|
|
||||||
|
### `subType=1` — `TECH_CREATE_CHANNEL`
|
||||||
|
|
||||||
|
Создание нового канала. Блок одновременно:
|
||||||
|
|
||||||
|
- является очередным шагом TECH-линии;
|
||||||
|
- является root нового канала;
|
||||||
|
- имеет `t=tech`;
|
||||||
|
- имеет `c_test5590=<canonical_channel_slug>`.
|
||||||
|
|
||||||
|
Начало body:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[4] lineCode = 0
|
||||||
|
[4] prevTechBlockNumber
|
||||||
|
[32] prevTechBlockHash32
|
||||||
|
[4] techSequence
|
||||||
|
```
|
||||||
|
|
||||||
|
Далее идут `channelName`, `channelDescription`, `channelType`, `channelTypeVersion` согласно формату CREATE_CHANNEL.
|
||||||
|
|
||||||
|
### `subType=2` — `TECH_FORK`
|
||||||
|
|
||||||
|
`TECH_FORK` фиксирует создание нового fork и является обычным следующим шагом TECH-линии.
|
||||||
|
|
||||||
|
#### `TECH_FORK` body (`version=1`)
|
||||||
|
|
||||||
Big-endian:
|
Big-endian:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[4] lineCode = 0
|
||||||
|
[4] prevTechBlockNumber
|
||||||
|
[32] prevTechBlockHash32
|
||||||
|
[4] techSequence
|
||||||
|
|
||||||
|
[32] parentBlockchainKey
|
||||||
|
[4] forkPointBlockNumber
|
||||||
|
[32] forkPointBlockHash32
|
||||||
|
[8] forkPointTimestampMs
|
||||||
|
[4] parentTipBlockNumber
|
||||||
|
[32] parentTipBlockHash32
|
||||||
|
[8] parentTipTimestampMs
|
||||||
|
[4] discardedBlocksCount
|
||||||
|
[1] reasonCode
|
||||||
|
[2] commentUtf8Length
|
||||||
|
[N] comment UTF-8
|
||||||
|
```
|
||||||
|
|
||||||
|
- `prevTechBlockNumber` — глобальный blockNumber предыдущего TECH-события; если до fork не было CREATE_CHANNEL/FORK, это `HEADER #0`;
|
||||||
|
- `prevTechBlockHash32` — hash этого блока;
|
||||||
|
- `techSequence` — предыдущий TECH sequence + 1; первый TECH-событийный блок после HEADER имеет `1`.
|
||||||
|
|
||||||
|
Остальная fork-часть:
|
||||||
|
|
||||||
- `parentBlockchainKey[32]` — public key предыдущего fork;
|
- `parentBlockchainKey[32]` — public key предыдущего fork;
|
||||||
- `forkPointBlockNumber[4]` — последний блок старой цепочки, сохранённый в новом fork;
|
- `forkPointBlockNumber[4]` — последний блок старой цепочки, сохранённый в новом fork;
|
||||||
- `forkPointBlockHash32[32]`;
|
- `forkPointBlockHash32[32]`;
|
||||||
@@ -28,28 +78,27 @@ Big-endian:
|
|||||||
- `parentTipBlockNumber[4]` — tip старой цепочки на момент начала ротации;
|
- `parentTipBlockNumber[4]` — tip старой цепочки на момент начала ротации;
|
||||||
- `parentTipBlockHash32[32]`;
|
- `parentTipBlockHash32[32]`;
|
||||||
- `parentTipTimestampMs[8]`;
|
- `parentTipTimestampMs[8]`;
|
||||||
- `discardedBlocksCount[4]` — `parentTipBlockNumber - forkPointBlockNumber`;
|
- `discardedBlocksCount[4] = parentTipBlockNumber - forkPointBlockNumber`;
|
||||||
- `reasonCode[1]`;
|
- `reasonCode[1]`;
|
||||||
- `commentUtf8Length[2]`;
|
- `commentUtf8Length[2]`;
|
||||||
- `comment[N]` — произвольный комментарий пользователя, максимум 1024 UTF-8 байт.
|
- `comment[N]` — максимум 1024 UTF-8 байт.
|
||||||
|
|
||||||
`reasonCode`:
|
`reasonCode`:
|
||||||
|
|
||||||
- `1` — `ROUTINE_ROTATION`: обычная смена пароля/ключей, компрометация не предполагается;
|
- `1` — `ROUTINE_ROTATION`;
|
||||||
- `2` — `POSSIBLE_COMPROMISE`: возможная компрометация, неизвестные записи не подтверждены;
|
- `2` — `POSSIBLE_COMPROMISE`;
|
||||||
- `3` — `CONFIRMED_COMPROMISE_ROLLBACK`: обнаружены нежелательные/чужие записи и выполнен rollback;
|
- `3` — `CONFIRMED_COMPROMISE_ROLLBACK`;
|
||||||
- `4` — `RECOVERY`: восстановление доступа recovery-механизмом.
|
- `4` — `RECOVERY`.
|
||||||
|
|
||||||
Правила:
|
Текущая схема ротации перепубликует выбранный префикс `0..forkPointBlockNumber` новым blockchain key и затем добавляет `TECH_FORK`. Перепубликованные Frame сохраняются байт-в-байт; внешняя ANS-104 owner/signature меняется. Новый blockchain key в body не дублируется: он определяется owner/signature нового DataItem.
|
||||||
|
|
||||||
- блоки `0..forkPointBlockNumber` в новом fork должны быть точными Frame-копиями выбранного префикса предыдущей цепочки;
|
## Серверная проверка TECH-линии
|
||||||
- `TECH_FORK` идёт сразу после этого префикса и является первым действительно новым Frame нового fork;
|
|
||||||
- если история сохранена полностью, `forkPointBlockNumber == parentTipBlockNumber` и `discardedBlocksCount == 0`;
|
|
||||||
- если сохраняется только genesis, новый fork содержит прежний `block 0`, а `block 1` является `TECH_FORK`;
|
|
||||||
- новый blockchain key в body не дублируется: он определяется owner/signature нового ANS-104 DataItem.
|
|
||||||
|
|
||||||
## Назначение
|
Для `TECH_CREATE_CHANNEL` и `TECH_FORK` сервер требует:
|
||||||
|
|
||||||
- инициализация блокчейна;
|
1. `lineCode=0`;
|
||||||
- управление набором каналов пользователя;
|
2. первый TECH-событийный блок ссылается на HEADER и имеет sequence `1`;
|
||||||
- фиксация происхождения нового fork и причины ротации/rollback.
|
3. каждый следующий ссылается на фактический текущий хвост TECH-линии;
|
||||||
|
4. номер предыдущего блока, его hash и sequence должны совпадать точно.
|
||||||
|
|
||||||
|
Таким образом CREATE_CHANNEL и FORK образуют единый проверяемый технический позвоночник пользователя.
|
||||||
|
|||||||
@@ -1,41 +1,68 @@
|
|||||||
# CONNECTION блоки (`type=3`, `version=1`)
|
# CONNECTION блоки (`type=3`, `version=1`)
|
||||||
|
|
||||||
CONNECTION-тип описывает социальные связи и подписки.
|
CONNECTION описывает историю социальных связей и подписок пользователя.
|
||||||
|
|
||||||
## Подтипы
|
## Подтипы
|
||||||
|
|
||||||
• `10/11` — `close_friend / unclose_friend` (близкий друг)
|
• `10/11` — `close_friend / unclose_friend` (близкий друг)
|
||||||
|
• `14/15` — `friend / unfriend` (друг)
|
||||||
• `20/21` — `contact / uncontact` (контакт)
|
• `20/21` — `contact / uncontact` (контакт)
|
||||||
• `30/31` — `follow / unfollow` (подписан)
|
• `30/31` — `follow / unfollow` (подписка)
|
||||||
• `40/41` — `spouse / unspouse` (супруг/супруга)
|
• `40/41` — `spouse / unspouse`
|
||||||
• `50/51` — `parent / unparent` (родитель)
|
• `50/51` — `parent / unparent`
|
||||||
• `52/53` — `child / unchild` (ребёнок)
|
• `52/53` — `child / unchild`
|
||||||
• `54/55` — `sibling / unsibling` (брат/сестра)
|
• `54/55` — `sibling / unsibling`
|
||||||
• `60/61` — `known_person / unknown_person` (знаю этого человека)
|
• `70/71` — `shine_confirmed / shine_unconfirmed`
|
||||||
• `70/71` — `shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий)
|
• `74/75` — `shine_seen / shine_unseen`
|
||||||
• `74/75` — `shine_seen / shine_unseen` (мало знаком, но видел сияющим)
|
• `80/81` — `official_account / unofficial_account`
|
||||||
|
|
||||||
## Общий формат payload
|
`60/61` — legacy/reserved и новым UI не создаются.
|
||||||
|
|
||||||
- line-поля (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`)
|
## Линия связей
|
||||||
- target (`toLogin`, `toForkNumber`, `toBlockGlobalNumber`, `toBlockHash32`)
|
|
||||||
|
|
||||||
## Бинарный target
|
У пользователя существует ровно одна линия `CONNECTION`. Отдельный `lineCode` в body не хранится: логический код линии всегда `0`.
|
||||||
|
|
||||||
|
Первый CONNECTION ссылается на `HEADER #0` и имеет `connectionSequence=1`. Каждый следующий обязан ссылаться на фактический текущий хвост CONNECTION-линии и увеличивать sequence ровно на `1`.
|
||||||
|
|
||||||
|
## Body (`version=1`)
|
||||||
|
|
||||||
|
Big-endian:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[1] toLoginLen (uint8)
|
[4] prevConnectionBlockNumber
|
||||||
|
[32] prevConnectionBlockHash32
|
||||||
|
[4] connectionSequence
|
||||||
|
|
||||||
|
[1] toLoginLen
|
||||||
[N] toLogin UTF-8
|
[N] toLogin UTF-8
|
||||||
[2] toForkNumber (uint16, BigEndian)
|
[2] toForkNumber
|
||||||
[4] toBlockGlobalNumber
|
[4] toBlockGlobalNumber
|
||||||
[32] toBlockHash32
|
[32] toBlockHash32
|
||||||
```
|
```
|
||||||
|
|
||||||
`toForkNumber` хранит fork, который видел автор связи (`1..999`), но не участвует в identity. Значения `0` и `1000+` недопустимы. Для связи на пользователя используется **реальный HEADER hash**: `toBlockGlobalNumber=0`, `toBlockHash32=SHA-256(Frame HEADER)`. Нулевой hash запрещён.
|
- `prevConnectionBlockNumber` — глобальный `blockNumber` предыдущего CONNECTION-блока.
|
||||||
|
- `prevConnectionBlockHash32` — SHA-256 Frame предыдущего CONNECTION-блока.
|
||||||
|
- `connectionSequence` — порядковый номер изменения связей `1..N`.
|
||||||
|
|
||||||
## Правила target
|
Сервер проверяет номер, hash и sequence относительно текущего хвоста CONNECTION-линии. Это позволяет выгрузить только связи пользователя и проверить, что внутри выбранной истории нет пропущенного шага.
|
||||||
|
|
||||||
- FRIEND/CONTACT обычно указывают на `HEADER` цели (`block 0`).
|
## Target
|
||||||
- FOLLOW указывает на root канала:
|
|
||||||
- `HEADER` для канала `0`;
|
`toForkNumber` хранит fork, который видел автор связи (`1..999`), но не входит в логическую identity цели. `0` зарезервирован как unknown/legacy; `1000+` недопустим.
|
||||||
- `CREATE_CHANNEL` для пользовательского канала.
|
|
||||||
- Для остальных типов связи (`SPOUSE/PARENT/CHILD/SIBLING`) используется тот же target-формат.
|
Для связи на пользователя обычно используется его HEADER:
|
||||||
|
|
||||||
|
```text
|
||||||
|
toBlockGlobalNumber=0
|
||||||
|
toBlockHash32=SHA-256(Frame HEADER)
|
||||||
|
```
|
||||||
|
|
||||||
|
Для FOLLOW на канал target указывает на root канала: HEADER для канала `0` либо `TECH_CREATE_CHANNEL` для пользовательского канала.
|
||||||
|
|
||||||
|
## ANS-104 тег
|
||||||
|
|
||||||
|
Каждый CONNECTION обязан иметь подписанный тег:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=conn
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# USER_PARAM блоки (`type=4`, `version=1`)
|
|
||||||
|
|
||||||
## Подтипы
|
|
||||||
|
|
||||||
1. `subType=1` — `USER_PARAM_TEXT_TEXT`
|
|
||||||
- хранит line-поля + `paramKey` + `paramValue`.
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
- сохранение пользовательского состояния (настройки клиента, синк-метки, курсоры чтения и т.д.).
|
|
||||||
|
|
||||||
## Практика
|
|
||||||
|
|
||||||
Для сложных структур удобно хранить JSON-строку в `paramValue` с версией схемы.
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# USER_PROFILE блоки (`type=4`, `version=1`)
|
||||||
|
|
||||||
|
`USER_PROFILE` хранит только данные, которыми пользователь описывает собственный профиль.
|
||||||
|
|
||||||
|
> В текущем Java/JS коде историческое внутреннее имя класса/метода может оставаться `UserParamBody` / `addBlockUserParam`. На уровне протокола и документации тип называется `USER_PROFILE`.
|
||||||
|
|
||||||
|
## Подтип
|
||||||
|
|
||||||
|
`subType=1` — текстовое поле профиля: `paramKey + paramValue`.
|
||||||
|
|
||||||
|
Примеры полей: имя, фамилия, `about`, аватар, web, телефон, роль аккаунта и другие свойства профиля, определённые приложением.
|
||||||
|
|
||||||
|
## Линия профиля
|
||||||
|
|
||||||
|
У пользователя существует ровно одна линия `USER_PROFILE`. Отдельный `lineCode` для неё не нужен и в body не хранится: логический код линии всегда `0`.
|
||||||
|
|
||||||
|
Первый `USER_PROFILE` ссылается на `HEADER #0` и имеет `profileSequence=1`. Каждый следующий блок обязан ссылаться на фактический текущий хвост профильной линии и увеличивать sequence ровно на `1`.
|
||||||
|
|
||||||
|
## Body (`version=1`)
|
||||||
|
|
||||||
|
Big-endian:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[4] prevProfileBlockNumber
|
||||||
|
[32] prevProfileBlockHash32
|
||||||
|
[4] profileSequence
|
||||||
|
[2] paramKeyLen
|
||||||
|
[N] paramKey UTF-8
|
||||||
|
[2] paramValueLen
|
||||||
|
[M] paramValue UTF-8
|
||||||
|
```
|
||||||
|
|
||||||
|
- `prevProfileBlockNumber` — **глобальный `blockNumber`** предыдущего блока профильной линии, а не локальный sequence.
|
||||||
|
- `prevProfileBlockHash32` — SHA-256 Frame предыдущего блока профильной линии.
|
||||||
|
- `profileSequence` — порядковый номер изменения профиля `1..N`.
|
||||||
|
|
||||||
|
Сервер проверяет одновременно номер предыдущего блока, его hash и непрерывность sequence. Поэтому выборка всех блоков `USER_PROFILE` позволяет обнаружить пропуск или боковую ветку.
|
||||||
|
|
||||||
|
## ANS-104 тег
|
||||||
|
|
||||||
|
Каждый `USER_PROFILE` обязан иметь подписанный тег:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=prof
|
||||||
|
```
|
||||||
@@ -14,13 +14,23 @@
|
|||||||
App=test5590
|
App=test5590
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Для фиксированных потоков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=tech # TECH: HEADER / CREATE_CHANNEL / FORK
|
||||||
|
t=conn # CONNECTION
|
||||||
|
t=prof # USER_PROFILE
|
||||||
|
```
|
||||||
|
|
||||||
Для блоков конкретного канала дополнительно:
|
Для блоков конкретного канала дополнительно:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
c_test5590=<canonical_channel_slug>
|
c_test5590=<canonical_channel_slug>
|
||||||
```
|
```
|
||||||
|
|
||||||
Теги входят в ANS-104 подпись пользователя. Старый тестовый тег `c` новым кодом не создаётся и не принимается как channel tag.
|
`TECH_CREATE_CHANNEL` несёт оба индекса: `t=tech` и `c_test5590=<slug>`. `STATUS_ACTION` не получает `t`-тег.
|
||||||
|
|
||||||
|
Теги входят в ANS-104 подпись пользователя. Сервер проверяет соответствие `t` фактическому `msg_type` и отклоняет лишний/неверный `t`. Старый тестовый тег `c` новым кодом не создаётся и не принимается как channel tag.
|
||||||
|
|
||||||
## Publisher modes
|
## Publisher modes
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## 2026-10-01 — Проверяемые USER_PROFILE / CONNECTION / TECH линии и signed `t` index
|
||||||
|
|
||||||
|
- Тип `4` в документации называется `USER_PROFILE`; внутренние исторические имена классов могут сохраняться. USER_PROFILE предназначен только для данных профиля.
|
||||||
|
- `USER_PROFILE` и `CONNECTION` получили обязательную фиксированную line-схему без сериализованного `lineCode`: `prevBlockNumber + prevBlockHash32 + sequence`. Первый шаг ссылается на HEADER и имеет sequence `1`.
|
||||||
|
- `TECH_FORK` включён в общую TECH-линию вместе с `TECH_CREATE_CHANNEL`; оба используют `lineCode=0`, предыдущий TECH block/hash и `techSequence`.
|
||||||
|
- Серверная line-валидация стала строгой: новый блок обязан продолжать фактический текущий хвост той же линии и иметь sequence `+1`; произвольная ссылка на существующий блок больше не принимается. Это также устраняет боковую ветку канала после `EDIT_POST`.
|
||||||
|
- Добавлены подписанные ANS-104 индексы: `t=prof`, `t=conn`, `t=tech`. `STATUS_ACTION` не изменён.
|
||||||
|
- В отдельном `shine-solana-arweave-viewer-v3` исправлен legacy-фильтр канала `c` → канонический `c_test5590`.
|
||||||
|
- `back32Hash` и дополнительные skip-ссылки не добавлялись.
|
||||||
|
- Формат USER_PROFILE/CONNECTION/TECH_FORK несовместим с предыдущей тестовой схемой; проект находится до production и миграция старых блоков не предусмотрена.
|
||||||
|
|
||||||
## 2026-09-27 — TECH_FORK v1
|
## 2026-09-27 — TECH_FORK v1
|
||||||
|
|
||||||
- Добавлен `type=0 / subType=2 / version=1` (`TECH_FORK`).
|
- Добавлен `type=0 / subType=2 / version=1` (`TECH_FORK`).
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- Importer всегда ищет только individual `App=test5590` DataItems.
|
- Importer всегда ищет только individual `App=test5590` DataItems.
|
||||||
- `App=test5590-batch` больше не используется.
|
- `App=test5590-batch` больше не используется.
|
||||||
- Channel tag: `c_test5590=<canonical_channel_slug>` вместо `c=...`.
|
- Channel tag: `c_test5590=<canonical_channel_slug>` вместо `c=...`.
|
||||||
|
- Фиксированные signed indexes: `t=tech`, `t=conn`, `t=prof`.
|
||||||
- Удалены `blocks.arweave_root_tx_id` и `arweave_block_import_queue.root_tx_id`.
|
- Удалены `blocks.arweave_root_tx_id` и `arweave_block_import_queue.root_tx_id`.
|
||||||
- Схема PostgreSQL: v25.
|
- Схема PostgreSQL: v25.
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
```text
|
```text
|
||||||
User
|
User
|
||||||
-> создаёт Frame v1
|
-> создаёт Frame v1
|
||||||
-> tags: App=test5590, при канале c_test5590=<slug>
|
-> tags: App=test5590; t=tech/conn/prof для фиксированных потоков; при канале c_test5590=<slug>
|
||||||
-> Ed25519 подписывает ANS-104 deep-hash
|
-> Ed25519 подписывает ANS-104 deep-hash
|
||||||
-> готовый DataItem
|
-> готовый DataItem
|
||||||
-> AddBlock
|
-> AddBlock
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# TODO — резервные ссылки `back32Hash` для SHiNE blockchain
|
||||||
|
|
||||||
|
Статус: **TODO / не реализовано**.
|
||||||
|
|
||||||
|
Цель этой идеи — повысить устойчивость проверки SHiNE blockchain и отдельных логических линий к ситуации, когда один или несколько DataItem временно или постоянно недоступны у конкретного Arweave gateway, CDN, сервера или другого источника.
|
||||||
|
|
||||||
|
Это **не замена** обычным ссылкам на предыдущий блок. Основная цепочка по-прежнему строится через `prevHash32` / `prevLineHash32`. `back32...` — только дополнительная резервная ссылка назад.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Глобальная цепочка пользователя
|
||||||
|
|
||||||
|
В общий Frame в будущем добавить одно поле:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[32] back32Hash32
|
||||||
|
```
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
back32Hash32 = HASH(global block N - 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
То есть блок `N` дополнительно содержит hash глобального блока `N-32`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
block #100
|
||||||
|
prevHash32 -> HASH(#99)
|
||||||
|
back32Hash32 -> HASH(#68)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для первых блоков
|
||||||
|
|
||||||
|
Если блока `N-32` ещё не существует, поле содержит 32 нулевых байта:
|
||||||
|
|
||||||
|
```text
|
||||||
|
#0 ... #31 -> back32Hash32 = ZERO_HASH_32
|
||||||
|
#32 -> HASH(#0)
|
||||||
|
#33 -> HASH(#1)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Номер блока отдельно НЕ хранить
|
||||||
|
|
||||||
|
Поле вида `back32BlockNumber` не нужно.
|
||||||
|
|
||||||
|
Номер однозначно вычисляется:
|
||||||
|
|
||||||
|
```text
|
||||||
|
back32BlockNumber = blockNumber - 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Поэтому хранить ещё 4 байта номера в каждом блоке бессмысленно.
|
||||||
|
|
||||||
|
Дополнительный размер глобального блока: **ровно +32 байта**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Логические линии
|
||||||
|
|
||||||
|
Для line-based блоков в будущем добавить ещё одно поле:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[32] back32LineHash32
|
||||||
|
```
|
||||||
|
|
||||||
|
Оно относится не к глобальной последовательности блоков, а к конкретной логической линии.
|
||||||
|
|
||||||
|
Используется для:
|
||||||
|
|
||||||
|
- `TEXT` — линия конкретного канала;
|
||||||
|
- `TECH` — техническая линия пользователя;
|
||||||
|
- `CONNECTION` — линия изменений связей;
|
||||||
|
- `USER_PROFILE` — линия изменений профиля.
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
back32LineHash32 = HASH(line item with sequence = currentSequence - 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример для канала:
|
||||||
|
|
||||||
|
```text
|
||||||
|
channel sequence 100
|
||||||
|
prevLineHash32 -> HASH(channel sequence 99)
|
||||||
|
back32LineHash32 -> HASH(channel sequence 68)
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример для профиля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
profileSequence = 50
|
||||||
|
prevProfileBlockHash32 -> HASH(profile sequence 49)
|
||||||
|
back32LineHash32 -> HASH(profile sequence 18)
|
||||||
|
```
|
||||||
|
|
||||||
|
Если элемента линии с `sequence - 32` ещё нет, используется:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ZERO_HASH_32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Номер элемента `-32` отдельно НЕ хранить
|
||||||
|
|
||||||
|
Он вычисляется из текущего sequence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
back32LineSequence = currentSequence - 32
|
||||||
|
```
|
||||||
|
|
||||||
|
А глобальный номер нужного старого блока при необходимости определяется по уже загруженной/проиндексированной линии.
|
||||||
|
|
||||||
|
Дополнительный размер line-based блока:
|
||||||
|
|
||||||
|
```text
|
||||||
|
+32 байта global back32Hash32
|
||||||
|
+32 байта back32LineHash32
|
||||||
|
= +64 байта
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Зачем это нужно
|
||||||
|
|
||||||
|
Обычная цепочка имеет только соседнюю связь:
|
||||||
|
|
||||||
|
```text
|
||||||
|
N -> N-1
|
||||||
|
```
|
||||||
|
|
||||||
|
Если блок `N-1` недоступен, проверить обычную связь следующего блока назад уже невозможно, хотя остальные данные могут быть доступны.
|
||||||
|
|
||||||
|
С дополнительной ссылкой получается:
|
||||||
|
|
||||||
|
```text
|
||||||
|
N -> N-1
|
||||||
|
N -> N-32
|
||||||
|
```
|
||||||
|
|
||||||
|
Поэтому даже если часть соседних блоков недоступна, можно получить дополнительную криптографическую связь с более ранней частью истории.
|
||||||
|
|
||||||
|
То же самое для отдельной линии:
|
||||||
|
|
||||||
|
```text
|
||||||
|
line N -> line N-1
|
||||||
|
line N -> line N-32
|
||||||
|
```
|
||||||
|
|
||||||
|
Это особенно полезно для каналов: если конкретный пост оказался недоступен или заблокирован одним источником, отсутствие этого DataItem не должно автоматически делать всю последующую историю канала полностью непроверяемой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Что этот механизм НЕ решает
|
||||||
|
|
||||||
|
`back32Hash` не означает, что отсутствующий блок восстановлен или проверен.
|
||||||
|
|
||||||
|
Если блока нет, его содержимое остаётся неизвестным.
|
||||||
|
|
||||||
|
Механизм даёт только дополнительный проверяемый мост между доступными частями истории.
|
||||||
|
|
||||||
|
Он также не заменяет:
|
||||||
|
|
||||||
|
- `prevHash32`;
|
||||||
|
- `prevLineHash32`;
|
||||||
|
- sequence логической линии;
|
||||||
|
- подпись ANS-104 DataItem;
|
||||||
|
- проверку текущего хвоста линии;
|
||||||
|
- отдельный механизм доказательства того, что скачан самый последний существующий блок/элемент линии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Почему шаг именно 32
|
||||||
|
|
||||||
|
Шаг `32` выбран как простой компромисс:
|
||||||
|
|
||||||
|
- всего 32 дополнительных байта на глобальную цепочку;
|
||||||
|
- ещё 32 байта только для line-based блоков;
|
||||||
|
- позволяет делать длинный резервный переход без сложной структуры skip-list;
|
||||||
|
- номер ссылки не надо хранить — он вычисляется арифметически;
|
||||||
|
- реализация и проверка остаются очень простыми.
|
||||||
|
|
||||||
|
В будущем шаг можно пересмотреть только через новую версию Frame/body, если появится убедительная причина.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. План реализации в будущем
|
||||||
|
|
||||||
|
Когда будет принято решение реализовать TODO:
|
||||||
|
|
||||||
|
1. добавить `back32Hash32` в новую версию общего Frame;
|
||||||
|
2. при создании блока вычислять hash глобального блока `N-32`;
|
||||||
|
3. добавить `back32LineHash32` в новые версии всех line-based body;
|
||||||
|
4. вычислять его по элементу той же линии с sequence `currentSequence-32`;
|
||||||
|
5. серверу проверять эти hashes при `AddBlock`;
|
||||||
|
6. импортерам/Arweave sync/viewer использовать резервные ссылки при проверке частично доступной истории;
|
||||||
|
7. обновить тесты бинарного формата и документацию;
|
||||||
|
8. не добавлять отдельные поля номера для ссылок `-32`.
|
||||||
|
|
||||||
|
До выполнения этих пунктов текущий канонический формат SHiNE остаётся без `back32Hash32` и `back32LineHash32`.
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
- `type=1` — `TEXT`
|
- `type=1` — `TEXT`
|
||||||
- `type=2` — `REACTION`
|
- `type=2` — `REACTION`
|
||||||
- `type=3` — `CONNECTION`
|
- `type=3` — `CONNECTION`
|
||||||
- `type=4` — `USER_PARAM`
|
- `type=4` — `USER_PROFILE`
|
||||||
|
|
||||||
## Верхнеуровневые типы первой итерации
|
## Верхнеуровневые типы первой итерации
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
- `1` — `TEXT`
|
- `1` — `TEXT`
|
||||||
- `2` — `REACTION`
|
- `2` — `REACTION`
|
||||||
- `3` — `CONNECTION`
|
- `3` — `CONNECTION`
|
||||||
- `4` — `USER_PARAM`
|
- `4` — `USER_PROFILE`
|
||||||
- `5` — `STATUS_ACTION`
|
- `5` — `STATUS_ACTION`
|
||||||
|
|
||||||
Отдельно:
|
Отдельно:
|
||||||
@@ -652,7 +652,7 @@
|
|||||||
| 1 | TEXT | старый код, новое расширенное смысловое описание |
|
| 1 | TEXT | старый код, новое расширенное смысловое описание |
|
||||||
| 2 | REACTION | старый |
|
| 2 | REACTION | старый |
|
||||||
| 3 | CONNECTION | старый |
|
| 3 | CONNECTION | старый |
|
||||||
| 4 | USER_PARAM | старый |
|
| 4 | USER_PROFILE | старый |
|
||||||
| 5 | STATUS_ACTION | первая итерация |
|
| 5 | STATUS_ACTION | первая итерация |
|
||||||
| 6 | CHANNEL_MEMBERSHIP | вынесено в отдельный отложенный черновик |
|
| 6 | CHANNEL_MEMBERSHIP | вынесено в отдельный отложенный черновик |
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
saveEncryptedUserSecrets,
|
saveEncryptedUserSecrets,
|
||||||
saveSessionMaterial,
|
saveSessionMaterial,
|
||||||
} from './key-vault.js';
|
} from './key-vault.js';
|
||||||
import { createAns104DataItem } 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';
|
||||||
|
|
||||||
const BCH_SUFFIX = '001';
|
const BCH_SUFFIX = '001';
|
||||||
@@ -48,10 +48,12 @@ const MSG_TYPE_TECH = 0;
|
|||||||
const MSG_TYPE_TEXT = 1;
|
const MSG_TYPE_TEXT = 1;
|
||||||
const MSG_TYPE_REACTION = 2;
|
const MSG_TYPE_REACTION = 2;
|
||||||
const MSG_TYPE_CONNECTION = 3;
|
const MSG_TYPE_CONNECTION = 3;
|
||||||
|
const MSG_TYPE_USER_PROFILE = 4;
|
||||||
const MSG_TYPE_STATUS_ACTION = 5;
|
const MSG_TYPE_STATUS_ACTION = 5;
|
||||||
|
|
||||||
const MSG_SUBTYPE_TECH_HEADER = 0;
|
const MSG_SUBTYPE_TECH_HEADER = 0;
|
||||||
const MSG_SUBTYPE_TECH_CREATE_CHANNEL = 1;
|
const MSG_SUBTYPE_TECH_CREATE_CHANNEL = 1;
|
||||||
|
const MSG_SUBTYPE_TECH_FORK = 2;
|
||||||
const MSG_SUBTYPE_TEXT_POST = 10;
|
const MSG_SUBTYPE_TEXT_POST = 10;
|
||||||
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
|
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
|
||||||
const MSG_SUBTYPE_TEXT_REPLY = 20;
|
const MSG_SUBTYPE_TEXT_REPLY = 20;
|
||||||
@@ -553,18 +555,19 @@ function makeHeaderBodyBytes({ login, initialBlockchainKey32 }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, key, value }) {
|
function makeUserParamBodyBytes({ prevProfileBlockNumber, prevProfileBlockHashHex, profileSequence, key, value }) {
|
||||||
const keyBytes = utf8Bytes(String(key || ''));
|
const keyBytes = utf8Bytes(String(key || ''));
|
||||||
const valueBytes = utf8Bytes(String(value || ''));
|
const valueBytes = utf8Bytes(String(value || ''));
|
||||||
const prevHashBytes = hexToBytes(prevLineHashHex);
|
const prevHashBytes = hexToBytes(prevProfileBlockHashHex);
|
||||||
if (!keyBytes.length || !valueBytes.length) throw new Error('Пустые key/value для блока параметра');
|
if (!keyBytes.length || !valueBytes.length) throw new Error('Пустые key/value для блока профиля');
|
||||||
if (prevHashBytes.length !== 32) throw new Error('prevLineHash должен быть 32 байта');
|
if (!Number.isInteger(Number(prevProfileBlockNumber)) || Number(prevProfileBlockNumber) < 0) throw new Error('Некорректный previous profile block number');
|
||||||
|
if (!Number.isInteger(Number(profileSequence)) || Number(profileSequence) < 1) throw new Error('Некорректный profile sequence');
|
||||||
|
if (prevHashBytes.length !== 32) throw new Error('prevProfileBlockHash должен быть 32 байта');
|
||||||
|
|
||||||
return concatBytes(
|
return concatBytes(
|
||||||
int32Bytes(lineCode),
|
int32Bytes(prevProfileBlockNumber),
|
||||||
int32Bytes(prevLineNumber),
|
|
||||||
prevHashBytes,
|
prevHashBytes,
|
||||||
int32Bytes(thisLineNumber),
|
int32Bytes(profileSequence),
|
||||||
int16Bytes(keyBytes.length),
|
int16Bytes(keyBytes.length),
|
||||||
keyBytes,
|
keyBytes,
|
||||||
int16Bytes(valueBytes.length),
|
int16Bytes(valueBytes.length),
|
||||||
@@ -653,15 +656,17 @@ function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeConnectionBodyBytes({
|
function makeConnectionBodyBytes({
|
||||||
lineCode = 0, prevLineNumber = -1, prevLineHashHex = ZERO64, thisLineNumber = -1,
|
prevConnectionBlockNumber, prevConnectionBlockHashHex, connectionSequence,
|
||||||
toLogin, toForkNumber, toBlockNumber, toBlockHashHex,
|
toLogin, toForkNumber, toBlockNumber, toBlockHashHex,
|
||||||
}) {
|
}) {
|
||||||
const loginBytes = targetLoginBytes(toLogin);
|
const loginBytes = targetLoginBytes(toLogin);
|
||||||
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'connection');
|
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'connection');
|
||||||
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'connection');
|
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'connection');
|
||||||
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'connection');
|
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'connection');
|
||||||
|
if (!Number.isInteger(Number(prevConnectionBlockNumber)) || Number(prevConnectionBlockNumber) < 0) throw new Error('Некорректный previous connection block number');
|
||||||
|
if (!Number.isInteger(Number(connectionSequence)) || Number(connectionSequence) < 1) throw new Error('Некорректный connection sequence');
|
||||||
return concatBytes(
|
return concatBytes(
|
||||||
int32Bytes(lineCode), int32Bytes(prevLineNumber), hexToBytes(normalizeHex32(prevLineHashHex)), int32Bytes(thisLineNumber),
|
int32Bytes(prevConnectionBlockNumber), hexToBytes(normalizeHex32(prevConnectionBlockHashHex)), int32Bytes(connectionSequence),
|
||||||
int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes,
|
int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -674,7 +679,7 @@ function targetLoginBytes(value) {
|
|||||||
|
|
||||||
function normalizeTargetForkNumber(value, actionName = 'target') {
|
function normalizeTargetForkNumber(value, actionName = 'target') {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
if (!Number.isInteger(n) || n < 1 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
if (!Number.isInteger(n) || n < 0 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,6 +889,51 @@ function resolveLatestLineStep(message) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bytesToHexLower(bytes) {
|
||||||
|
return Array.from(bytes || [], (value) => Number(value).toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoredLineMeta(blockBytesB64, { fixedLine = false } = {}) {
|
||||||
|
const raw = base64ToBytes(String(blockBytesB64 || ''));
|
||||||
|
const frame = parseAns104DataItem(raw).data;
|
||||||
|
const bodyOffset = 56;
|
||||||
|
const minLength = fixedLine ? bodyOffset + 40 : bodyOffset + 44;
|
||||||
|
if (!(frame instanceof Uint8Array) || frame.length < minLength) throw new Error('Некорректный line-блок');
|
||||||
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
||||||
|
if (fixedLine) {
|
||||||
|
return {
|
||||||
|
lineCode: 0,
|
||||||
|
prevLineNumber: view.getInt32(bodyOffset, false),
|
||||||
|
prevLineHashHex: bytesToHexLower(frame.slice(bodyOffset + 4, bodyOffset + 36)),
|
||||||
|
lineSequence: view.getInt32(bodyOffset + 36, false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lineCode: view.getInt32(bodyOffset, false),
|
||||||
|
prevLineNumber: view.getInt32(bodyOffset + 4, false),
|
||||||
|
prevLineHashHex: bytesToHexLower(frame.slice(bodyOffset + 8, bodyOffset + 40)),
|
||||||
|
lineSequence: view.getInt32(bodyOffset + 40, false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeIndexTagValue(msgType) {
|
||||||
|
switch (Number(msgType)) {
|
||||||
|
case MSG_TYPE_TECH: return 'tech';
|
||||||
|
case MSG_TYPE_CONNECTION: return 'conn';
|
||||||
|
case MSG_TYPE_USER_PROFILE: return 'prof';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLineConflictError(error) {
|
||||||
|
return new Set([
|
||||||
|
'BAD_PREV_LINE_NUMBER',
|
||||||
|
'BAD_PREV_LINE_HASH',
|
||||||
|
'BAD_LINE_SEQUENCE',
|
||||||
|
'BAD_LINE_STATE',
|
||||||
|
]).has(String(error?.code || '').toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
|
function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
|
||||||
const prevHashBytes = hexToBytes(normalizeHex32(prevBlockHashHex));
|
const prevHashBytes = hexToBytes(normalizeHex32(prevBlockHashHex));
|
||||||
const body = bodyBytes || new Uint8Array(0);
|
const body = bodyBytes || new Uint8Array(0);
|
||||||
@@ -919,6 +969,8 @@ export class AuthService {
|
|||||||
this.eventListeners = new Map();
|
this.eventListeners = new Map();
|
||||||
this.wsEventUnsubscribers = new Map();
|
this.wsEventUnsubscribers = new Map();
|
||||||
this.headerHashCache = new Map();
|
this.headerHashCache = new Map();
|
||||||
|
this.lineTailCache = new Map();
|
||||||
|
this.lineWriteQueues = new Map();
|
||||||
this.writeLocks = new Map();
|
this.writeLocks = new Map();
|
||||||
this.passwordKeyBundleCache = new Map();
|
this.passwordKeyBundleCache = new Map();
|
||||||
this.passwordKeyBundleInFlight = new Map();
|
this.passwordKeyBundleInFlight = new Map();
|
||||||
@@ -951,6 +1003,8 @@ export class AuthService {
|
|||||||
this.serverUrl = normalized;
|
this.serverUrl = normalized;
|
||||||
this.ws = new WsJsonClient(this.serverUrl);
|
this.ws = new WsJsonClient(this.serverUrl);
|
||||||
this.headerHashCache = new Map();
|
this.headerHashCache = new Map();
|
||||||
|
this.lineTailCache = new Map();
|
||||||
|
this.lineWriteQueues = new Map();
|
||||||
this.writeLocks.clear();
|
this.writeLocks.clear();
|
||||||
this.bindRegisteredEventsToCurrentWs();
|
this.bindRegisteredEventsToCurrentWs();
|
||||||
if (clearSessionContext) this.clearActiveSessionContext();
|
if (clearSessionContext) this.clearActiveSessionContext();
|
||||||
@@ -1142,10 +1196,8 @@ export class AuthService {
|
|||||||
async createSessionFromImportedSecrets(login, secrets) {
|
async createSessionFromImportedSecrets(login, secrets) {
|
||||||
const cleanLogin = (login || '').trim();
|
const cleanLogin = (login || '').trim();
|
||||||
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
||||||
const clientKey = String(secrets?.clientKey || '').trim();
|
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
|
||||||
const blockchainKey = String(secrets?.blockchainKey || '').trim();
|
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
|
||||||
if (!clientKey) throw new Error('В QR-коде нет ключа доступа для входа');
|
|
||||||
if (!blockchainKey) throw new Error('В QR-коде нет ключа блокчейна');
|
|
||||||
|
|
||||||
const privateKey = await importPkcs8Ed25519(clientKey);
|
const privateKey = await importPkcs8Ed25519(clientKey);
|
||||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
||||||
@@ -1221,7 +1273,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async persistSelectedKeys(login, storagePwd, keyBundle) {
|
async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) {
|
||||||
let currentSecrets = {};
|
let currentSecrets = {};
|
||||||
try {
|
try {
|
||||||
const loaded = await loadEncryptedUserSecrets(login, storagePwd);
|
const loaded = await loadEncryptedUserSecrets(login, storagePwd);
|
||||||
@@ -1232,23 +1284,12 @@ export class AuthService {
|
|||||||
// Если контейнера ещё нет или пароль новый для этого логина — создадим новый ниже.
|
// Если контейнера ещё нет или пароль новый для этого логина — создадим новый ниже.
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientKey = String(keyBundle?.clientPair?.privatePkcs8B64 || '').trim();
|
|
||||||
const blockchainKey = String(keyBundle?.blockchainPair?.privatePkcs8B64 || '').trim();
|
|
||||||
if (!clientKey || !blockchainKey) {
|
|
||||||
throw new Error('Для входа нужно сохранить client key и blockchain key');
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = {
|
const secrets = {
|
||||||
...currentSecrets,
|
...currentSecrets,
|
||||||
clientKey,
|
clientKey: keyBundle.clientPair.privatePkcs8B64,
|
||||||
blockchainKey,
|
|
||||||
};
|
};
|
||||||
// SAWD-v1 wallet зависит от blockchain key. При смене ключа старый кэш
|
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
|
||||||
// нельзя переносить в новую identity: он будет лениво выведен заново.
|
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
|
||||||
if (String(currentSecrets?.blockchainKey || '').trim() !== blockchainKey) {
|
|
||||||
delete secrets.arweaveWallet;
|
|
||||||
}
|
|
||||||
delete secrets.rootKey;
|
|
||||||
await saveEncryptedUserSecrets(login, storagePwd, secrets);
|
await saveEncryptedUserSecrets(login, storagePwd, secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1773,6 +1814,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ansTags = [{ name: 'App', value: 'test5590' }];
|
const ansTags = [{ name: 'App', value: 'test5590' }];
|
||||||
|
const typeIndexTag = typeIndexTagValue(msgType);
|
||||||
|
if (typeIndexTag) ansTags.push({ name: 't', 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: 'c_test5590', value: cleanChannelSlug });
|
||||||
|
|
||||||
@@ -1990,53 +2033,17 @@ export class AuthService {
|
|||||||
const selector = channel || {};
|
const selector = channel || {};
|
||||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||||
const root = Number(selector?.channelRootBlockNumber);
|
const root = Number(selector?.channelRootBlockNumber);
|
||||||
const key = `repost:${cleanLogin}:${owner}:${root}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}`;
|
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||||
|
|
||||||
return this.runWriteLocked(key, async () => {
|
return this.runLineWriteLocked(key, async () => {
|
||||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
if (!owner || !Number.isFinite(root) || root < 0) throw new Error('Invalid channel selector');
|
const tail = await this.resolveChannelLineTail({ login: cleanLogin, blockchainName, channel: selector });
|
||||||
if (owner !== blockchainName) throw new Error('Repost is allowed only to your own channels');
|
|
||||||
|
|
||||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
|
||||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
|
||||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
|
||||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
|
||||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === root);
|
|
||||||
if (!rootChannel) throw new Error('Channel root not found');
|
|
||||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
|
||||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
let prevLineNumber = root;
|
|
||||||
let prevLineHashHex = rootHashHex;
|
|
||||||
let thisLineNumber = 0;
|
|
||||||
try {
|
|
||||||
const latestPayload = await this.getChannelMessages({
|
|
||||||
ownerBlockchainName: owner,
|
|
||||||
channelRootBlockNumber: root,
|
|
||||||
channelRootBlockHash: rootHashHex,
|
|
||||||
}, 1, 'desc', cleanLogin);
|
|
||||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
|
||||||
const latestBlockNumber = Number(latestMessage?.messageRef?.blockNumber);
|
|
||||||
const latestBlockHash = normalizeHex32(latestMessage?.messageRef?.blockHash, '');
|
|
||||||
const latestLineStep = resolveLatestLineStep(latestMessage);
|
|
||||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
|
||||||
prevLineNumber = latestBlockNumber;
|
|
||||||
prevLineHashHex = latestBlockHash;
|
|
||||||
thisLineNumber = Number.isFinite(latestLineStep)
|
|
||||||
? Math.max(0, latestLineStep + 1)
|
|
||||||
: 1;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fallback to root anchor
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyBytes = makeTextRepostBodyBytes({
|
const bodyBytes = makeTextRepostBodyBytes({
|
||||||
lineCode: root,
|
lineCode: tail.lineCode,
|
||||||
prevLineNumber,
|
prevLineNumber: tail.prevLineNumber,
|
||||||
prevLineHashHex,
|
prevLineHashHex: tail.prevLineHashHex,
|
||||||
thisLineNumber,
|
thisLineNumber: tail.thisLineNumber,
|
||||||
toLogin: target.login,
|
toLogin: target.login,
|
||||||
toForkNumber: target.forkNumber,
|
toForkNumber: target.forkNumber,
|
||||||
toBlockNumber: target.blockNumber,
|
toBlockNumber: target.blockNumber,
|
||||||
@@ -2044,15 +2051,17 @@ export class AuthService {
|
|||||||
text: cleanText,
|
text: cleanText,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.addBlockSigned({
|
const payload = await this.addBlockSigned({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
msgType: MSG_TYPE_TEXT,
|
msgType: MSG_TYPE_TEXT,
|
||||||
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
|
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
|
||||||
msgVersion: 1,
|
msgVersion: 1,
|
||||||
bodyBytes,
|
bodyBytes,
|
||||||
channelSlug,
|
channelSlug: tail.channelSlug,
|
||||||
});
|
});
|
||||||
|
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||||
|
return payload;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2067,82 +2076,57 @@ export class AuthService {
|
|||||||
const cleanLogin = String(login || '').trim();
|
const cleanLogin = String(login || '').trim();
|
||||||
const cleanText = String(text || '').trim();
|
const cleanText = String(text || '').trim();
|
||||||
const target = normalizeMessageRefTarget(message, 'edit');
|
const target = normalizeMessageRefTarget(message, 'edit');
|
||||||
const lockKey = `edit:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}:${isChannelPost ? 'post' : 'reply'}`;
|
|
||||||
|
|
||||||
return this.runWriteLocked(lockKey, async () => {
|
if (isChannelPost) {
|
||||||
if (isChannelPost) {
|
const selector = channel || {};
|
||||||
const selector = channel || {};
|
const ownerBlockchainName = String(selector?.ownerBlockchainName || target.blockchainName || '').trim();
|
||||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || target.blockchainName || '').trim();
|
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
if (!ownerBlockchainName || !Number.isInteger(lineCode) || lineCode < 0) {
|
||||||
if (!ownerBlockchainName || !Number.isFinite(lineCode) || lineCode < 0) {
|
throw new Error('Invalid channel selector for edit');
|
||||||
throw new Error('Invalid channel selector for edit');
|
}
|
||||||
}
|
const lockKey = `channel-line:${cleanLogin}:${ownerBlockchainName}:${lineCode}`;
|
||||||
|
return this.runLineWriteLocked(lockKey, async () => {
|
||||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
const tail = await this.resolveChannelLineTail({
|
||||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, ownerBlockchainName);
|
login: cleanLogin,
|
||||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
blockchainName,
|
||||||
if (rootChannel) {
|
channel: {
|
||||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
...selector,
|
||||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prevLineNumber = lineCode;
|
|
||||||
let prevLineHashHex = rootHashHex;
|
|
||||||
let thisLineNumber = 1;
|
|
||||||
try {
|
|
||||||
const latestPayload = await this.getChannelMessages({
|
|
||||||
ownerBlockchainName,
|
ownerBlockchainName,
|
||||||
channelRootBlockNumber: lineCode,
|
channelRootBlockNumber: lineCode,
|
||||||
channelRootBlockHash: rootHashHex,
|
},
|
||||||
}, 1, 'desc', cleanLogin);
|
});
|
||||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
|
||||||
const latestVersions = Array.isArray(latestMessage?.versions) ? latestMessage.versions : [];
|
|
||||||
const latestVersion = latestVersions[latestVersions.length - 1] || null;
|
|
||||||
const latestBlockNumber = Number(latestVersion?.blockNumber ?? latestMessage?.messageRef?.blockNumber);
|
|
||||||
const latestBlockHash = normalizeHex32(latestVersion?.blockHash ?? latestMessage?.messageRef?.blockHash, '');
|
|
||||||
const latestLineStep = resolveLatestLineStep(latestMessage);
|
|
||||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
|
||||||
prevLineNumber = latestBlockNumber;
|
|
||||||
prevLineHashHex = latestBlockHash;
|
|
||||||
thisLineNumber = Number.isFinite(latestLineStep)
|
|
||||||
? Math.max(0, latestLineStep)
|
|
||||||
: 1;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fallback to root anchor
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyBytes = makeTextEditPostBodyBytes({
|
const bodyBytes = makeTextEditPostBodyBytes({
|
||||||
lineCode,
|
lineCode: tail.lineCode,
|
||||||
prevLineNumber,
|
prevLineNumber: tail.prevLineNumber,
|
||||||
prevLineHashHex,
|
prevLineHashHex: tail.prevLineHashHex,
|
||||||
thisLineNumber,
|
thisLineNumber: tail.thisLineNumber,
|
||||||
toLogin: target.login,
|
|
||||||
toBlockNumber: target.blockNumber,
|
toBlockNumber: target.blockNumber,
|
||||||
toBlockHashHex: target.blockHash,
|
toBlockHashHex: target.blockHash,
|
||||||
text: cleanText,
|
text: cleanText,
|
||||||
});
|
});
|
||||||
|
const payload = await this.addBlockSigned({
|
||||||
return this.addBlockSigned({
|
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
msgType: MSG_TYPE_TEXT,
|
msgType: MSG_TYPE_TEXT,
|
||||||
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
|
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||||
msgVersion: 1,
|
msgVersion: 1,
|
||||||
bodyBytes,
|
bodyBytes,
|
||||||
channelSlug,
|
channelSlug: tail.channelSlug,
|
||||||
});
|
});
|
||||||
}
|
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||||
|
return payload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockKey = `edit-reply:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}`;
|
||||||
|
return this.runWriteLocked(lockKey, async () => {
|
||||||
const bodyBytes = makeTextEditReplyBodyBytes({
|
const bodyBytes = makeTextEditReplyBodyBytes({
|
||||||
toBlockNumber: target.blockNumber,
|
toBlockNumber: target.blockNumber,
|
||||||
toBlockHashHex: target.blockHash,
|
toBlockHashHex: target.blockHash,
|
||||||
text: cleanText,
|
text: cleanText,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.addBlockSigned({
|
return this.addBlockSigned({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
@@ -2196,35 +2180,21 @@ export class AuthService {
|
|||||||
throw new Error('Invalid target block number');
|
throw new Error('Invalid target block number');
|
||||||
}
|
}
|
||||||
const seedHash = normalizeHex32(targetBlockHashHex, ZERO64);
|
const seedHash = normalizeHex32(targetBlockHashHex, ZERO64);
|
||||||
const key = `${unfollow ? 'unfollow-channel' : 'follow-channel'}:${cleanLogin}:${cleanTargetBch}:${cleanTargetBlockNumber}:${seedHash}`;
|
let targetHashHex = seedHash;
|
||||||
|
if (targetHashHex === ZERO64) {
|
||||||
|
targetHashHex = cleanTargetBlockNumber === 0
|
||||||
|
? await this.resolveHeaderHashForBlockchain(cleanTargetBch)
|
||||||
|
: await this.getBlockHashByNumber(cleanTargetBch, cleanTargetBlockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
return this.runWriteLocked(key, async () => {
|
return this.addBlockConnectionTarget({
|
||||||
let targetHashHex = seedHash;
|
login: cleanLogin,
|
||||||
if (targetHashHex === ZERO64) {
|
storagePwd,
|
||||||
targetHashHex = cleanTargetBlockNumber === 0
|
subType: unfollow ? MSG_SUBTYPE_CONNECTION_UNFOLLOW : MSG_SUBTYPE_CONNECTION_FOLLOW,
|
||||||
? await this.resolveHeaderHashForBlockchain(cleanTargetBch)
|
toLogin: cleanTargetLogin,
|
||||||
: await this.getBlockHashByNumber(cleanTargetBch, cleanTargetBlockNumber);
|
toForkNumber: forkNumberFromBlockchainNameValue(cleanTargetBch),
|
||||||
}
|
toBlockNumber: cleanTargetBlockNumber,
|
||||||
|
toBlockHashHex: targetHashHex,
|
||||||
const bodyBytes = makeConnectionBodyBytes({
|
|
||||||
lineCode: 0,
|
|
||||||
prevLineNumber: -1,
|
|
||||||
prevLineHashHex: ZERO64,
|
|
||||||
thisLineNumber: -1,
|
|
||||||
toLogin: cleanTargetLogin,
|
|
||||||
toForkNumber: forkNumberFromBlockchainNameValue(cleanTargetBch),
|
|
||||||
toBlockNumber: cleanTargetBlockNumber,
|
|
||||||
toBlockHashHex: targetHashHex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.addBlockSigned({
|
|
||||||
login: cleanLogin,
|
|
||||||
storagePwd,
|
|
||||||
msgType: MSG_TYPE_CONNECTION,
|
|
||||||
msgSubType: unfollow ? MSG_SUBTYPE_CONNECTION_UNFOLLOW : MSG_SUBTYPE_CONNECTION_FOLLOW,
|
|
||||||
msgVersion: 1,
|
|
||||||
bodyBytes,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2270,6 +2240,123 @@ export class AuthService {
|
|||||||
return headerHash;
|
return headerHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMyBlockchainPage({ beforeBlock = null, limit = 100, includeBlockBytes = true } = {}) {
|
||||||
|
const response = await this.ws.request('GetMyBlockchain', { beforeBlock, limit, includeBlockBytes });
|
||||||
|
if (response.status !== 200) throw opError('GetMyBlockchain', response);
|
||||||
|
return response.payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findLatestOwnLineBlock({
|
||||||
|
blockchainName,
|
||||||
|
msgType,
|
||||||
|
subTypes = null,
|
||||||
|
lineCode = null,
|
||||||
|
fixedLine = false,
|
||||||
|
stopAtBlock = 0,
|
||||||
|
}) {
|
||||||
|
const cleanBch = String(blockchainName || '').trim();
|
||||||
|
if (!cleanBch) throw new Error('Missing blockchainName for line lookup');
|
||||||
|
const subtypeSet = subTypes == null ? null : new Set(Array.from(subTypes, (value) => Number(value)));
|
||||||
|
const minBlock = Math.max(0, Number(stopAtBlock) || 0);
|
||||||
|
let beforeBlock = null;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
const page = await this.getMyBlockchainPage({ beforeBlock, limit: 100, includeBlockBytes: true });
|
||||||
|
const pageBch = String(page?.blockchainName || '').trim();
|
||||||
|
if (pageBch && pageBch !== cleanBch) throw new Error('GetMyBlockchain returned another blockchain');
|
||||||
|
const blocks = Array.isArray(page?.blocks) ? page.blocks : [];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const blockNumber = Number(block?.blockNumber);
|
||||||
|
if (!Number.isInteger(blockNumber) || blockNumber <= minBlock) continue;
|
||||||
|
if (Number(block?.msgType) !== Number(msgType)) continue;
|
||||||
|
if (subtypeSet && !subtypeSet.has(Number(block?.msgSubType))) continue;
|
||||||
|
if (!block?.blockBytesB64) continue;
|
||||||
|
const meta = parseStoredLineMeta(block.blockBytesB64, { fixedLine });
|
||||||
|
if (lineCode != null && Number(meta.lineCode) !== Number(lineCode)) continue;
|
||||||
|
return {
|
||||||
|
blockNumber,
|
||||||
|
blockHash: normalizeHex32(block?.blockHash, ZERO64),
|
||||||
|
lineSequence: Number(meta.lineSequence),
|
||||||
|
lineCode: Number(meta.lineCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const next = page?.nextBeforeBlock;
|
||||||
|
if (next == null || !Number.isInteger(Number(next)) || Number(next) <= minBlock) return null;
|
||||||
|
beforeBlock = Number(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveFixedLineTail({ blockchainName, kind }) {
|
||||||
|
const cleanBch = String(blockchainName || '').trim();
|
||||||
|
const kindKey = String(kind || '').trim().toLowerCase();
|
||||||
|
const config = kindKey === 'profile'
|
||||||
|
? { msgType: MSG_TYPE_USER_PROFILE, tag: 'prof' }
|
||||||
|
: kindKey === 'connections'
|
||||||
|
? { msgType: MSG_TYPE_CONNECTION, tag: 'conn' }
|
||||||
|
: kindKey === 'tech'
|
||||||
|
? { msgType: MSG_TYPE_TECH, tag: 'tech', subTypes: [MSG_SUBTYPE_TECH_CREATE_CHANNEL, MSG_SUBTYPE_TECH_FORK] }
|
||||||
|
: null;
|
||||||
|
if (!config) throw new Error(`Unknown fixed line kind: ${kind}`);
|
||||||
|
|
||||||
|
const cacheKey = `${cleanBch}|${kindKey}`;
|
||||||
|
const cached = this.lineTailCache.get(cacheKey);
|
||||||
|
if (cached) return { ...cached };
|
||||||
|
|
||||||
|
const latest = await this.findLatestOwnLineBlock({
|
||||||
|
blockchainName: cleanBch,
|
||||||
|
msgType: config.msgType,
|
||||||
|
subTypes: config.subTypes || null,
|
||||||
|
lineCode: kindKey === 'tech' ? 0 : null,
|
||||||
|
fixedLine: kindKey !== 'tech',
|
||||||
|
stopAtBlock: 0,
|
||||||
|
});
|
||||||
|
if (latest) {
|
||||||
|
const result = {
|
||||||
|
prevLineNumber: latest.blockNumber,
|
||||||
|
prevLineHashHex: latest.blockHash,
|
||||||
|
thisLineNumber: latest.lineSequence + 1,
|
||||||
|
};
|
||||||
|
this.lineTailCache.set(cacheKey, result);
|
||||||
|
return { ...result };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerHash = await this.resolveHeaderHashForBlockchain(cleanBch);
|
||||||
|
if (headerHash === ZERO64) throw new Error('HEADER блокчейна не найден');
|
||||||
|
const result = { prevLineNumber: 0, prevLineHashHex: headerHash, thisLineNumber: 1 };
|
||||||
|
this.lineTailCache.set(cacheKey, result);
|
||||||
|
return { ...result };
|
||||||
|
}
|
||||||
|
|
||||||
|
rememberLineTail(blockchainName, kind, payload, currentSequence) {
|
||||||
|
const cleanBch = String(blockchainName || '').trim();
|
||||||
|
const acceptedNumber = Number(payload?.serverLastGlobalNumber);
|
||||||
|
const acceptedHash = normalizeHex32(payload?.serverLastGlobalHash, ZERO64);
|
||||||
|
if (!cleanBch || !Number.isInteger(acceptedNumber) || acceptedNumber < 0 || acceptedHash === ZERO64) return;
|
||||||
|
this.lineTailCache.set(`${cleanBch}|${String(kind || '').trim().toLowerCase()}`, {
|
||||||
|
prevLineNumber: acceptedNumber,
|
||||||
|
prevLineHashHex: acceptedHash,
|
||||||
|
thisLineNumber: Number(currentSequence) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async runLineWriteLocked(lockKey, buildAndSend) {
|
||||||
|
const key = String(lockKey || '').trim() || 'line-write';
|
||||||
|
const previous = this.lineWriteQueues.get(key) || Promise.resolve();
|
||||||
|
const task = previous.catch(() => {}).then(async () => {
|
||||||
|
try {
|
||||||
|
return await buildAndSend();
|
||||||
|
} catch (error) {
|
||||||
|
if (!isLineConflictError(error)) throw error;
|
||||||
|
this.lineTailCache.clear();
|
||||||
|
return buildAndSend();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.lineWriteQueues.set(key, task);
|
||||||
|
return task.finally(() => {
|
||||||
|
if (this.lineWriteQueues.get(key) === task) this.lineWriteQueues.delete(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async listOwnChannelsForBlockchain(login, blockchainName) {
|
async listOwnChannelsForBlockchain(login, blockchainName) {
|
||||||
const feed = await this.listSubscriptionsFeed(login, 500);
|
const feed = await this.listSubscriptionsFeed(login, 500);
|
||||||
const own = feed?.ownedChannels || [];
|
const own = feed?.ownedChannels || [];
|
||||||
@@ -2314,34 +2401,11 @@ export class AuthService {
|
|||||||
: normalizeChannelDescription(channelDescription);
|
: normalizeChannelDescription(channelDescription);
|
||||||
const channelSlug = toCanonicalChannelSlug(cleanChannelName);
|
const channelSlug = toCanonicalChannelSlug(cleanChannelName);
|
||||||
const typeVersion = Number(channelTypeVersion);
|
const typeVersion = Number(channelTypeVersion);
|
||||||
const key = `create-channel:${cleanLogin}:${typeCode}:${channelSlug || cleanChannelName.toLowerCase()}`;
|
const key = `tech-line:${cleanLogin}`;
|
||||||
return this.runWriteLocked(key, async () => {
|
return this.runLineWriteLocked(key, async () => {
|
||||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
const userLastGlobalNumber = Number(user?.serverLastGlobalNumber);
|
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'tech' });
|
||||||
const userLastGlobalHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64);
|
|
||||||
|
|
||||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
|
||||||
const createdChannels = ownChannels
|
|
||||||
.filter((item) => item.rootBlockNumber > 0)
|
|
||||||
.sort((a, b) => a.rootBlockNumber - b.rootBlockNumber);
|
|
||||||
|
|
||||||
let prevLineNumber = 0;
|
|
||||||
let prevLineHashHex = (
|
|
||||||
Number.isFinite(userLastGlobalNumber) &&
|
|
||||||
userLastGlobalNumber === 0 &&
|
|
||||||
userLastGlobalHash !== ZERO64
|
|
||||||
)
|
|
||||||
? userLastGlobalHash
|
|
||||||
: await this.resolveHeaderHashForBlockchain(blockchainName);
|
|
||||||
let thisLineNumber = 1;
|
|
||||||
|
|
||||||
if (createdChannels.length > 0) {
|
|
||||||
const last = createdChannels[createdChannels.length - 1];
|
|
||||||
prevLineNumber = last.rootBlockNumber;
|
|
||||||
prevLineHashHex = normalizeHex32(last.rootBlockHash, ZERO64);
|
|
||||||
thisLineNumber = createdChannels.length + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await this.addBlockSigned({
|
const payload = await this.addBlockSigned({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
@@ -2352,15 +2416,16 @@ export class AuthService {
|
|||||||
channelSlug,
|
channelSlug,
|
||||||
bodyBytes: makeCreateChannelBodyBytes({
|
bodyBytes: makeCreateChannelBodyBytes({
|
||||||
lineCode: 0,
|
lineCode: 0,
|
||||||
prevLineNumber,
|
prevLineNumber: tail.prevLineNumber,
|
||||||
prevLineHashHex,
|
prevLineHashHex: tail.prevLineHashHex,
|
||||||
thisLineNumber,
|
thisLineNumber: tail.thisLineNumber,
|
||||||
channelName: cleanChannelName,
|
channelName: cleanChannelName,
|
||||||
channelDescription: cleanChannelDescription,
|
channelDescription: cleanChannelDescription,
|
||||||
channelType: typeCode,
|
channelType: typeCode,
|
||||||
channelTypeVersion: typeVersion,
|
channelTypeVersion: typeVersion,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
this.rememberLineTail(blockchainName, 'tech', payload, tail.thisLineNumber);
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
ownerBlockchainName: blockchainName,
|
ownerBlockchainName: blockchainName,
|
||||||
@@ -2381,7 +2446,7 @@ export class AuthService {
|
|||||||
const selector = channel || {};
|
const selector = channel || {};
|
||||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || '').trim();
|
const ownerBlockchainName = String(selector?.ownerBlockchainName || '').trim();
|
||||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||||
if (!ownerBlockchainName || !Number.isFinite(lineCode) || lineCode < 0) {
|
if (!ownerBlockchainName || !Number.isInteger(lineCode) || lineCode < 0) {
|
||||||
throw new Error('Invalid channel selector');
|
throw new Error('Invalid channel selector');
|
||||||
}
|
}
|
||||||
if (ownerBlockchainName !== blockchainName) {
|
if (ownerBlockchainName !== blockchainName) {
|
||||||
@@ -2398,52 +2463,70 @@ export class AuthService {
|
|||||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||||
}
|
}
|
||||||
if (!channelSlug) throw new Error('Cannot resolve canonical channel slug');
|
if (!channelSlug) throw new Error('Cannot resolve canonical channel slug');
|
||||||
|
if (rootHashHex === ZERO64) throw new Error('Cannot resolve channel root hash');
|
||||||
|
|
||||||
let prevLineNumber = lineCode;
|
const cacheKey = `${blockchainName}|channel:${lineCode}`;
|
||||||
let prevLineHashHex = rootHashHex;
|
const cached = this.lineTailCache.get(cacheKey);
|
||||||
let thisLineNumber = 0;
|
if (cached) {
|
||||||
try {
|
return {
|
||||||
const latestPayload = await this.getChannelMessages({
|
|
||||||
ownerBlockchainName,
|
ownerBlockchainName,
|
||||||
channelRootBlockNumber: lineCode,
|
lineCode,
|
||||||
channelRootBlockHash: rootHashHex,
|
rootHashHex,
|
||||||
}, 1, 'desc', login);
|
channelSlug,
|
||||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
...cached,
|
||||||
const latestMeta = (Array.isArray(latestPayload?.metaEvents) ? latestPayload.metaEvents : [])
|
};
|
||||||
.slice()
|
|
||||||
.sort((a, b) => Number(b?.messageRef?.blockNumber || -1) - Number(a?.messageRef?.blockNumber || -1))[0] || null;
|
|
||||||
const latestMessageBlock = Number(latestMessage?.messageRef?.blockNumber);
|
|
||||||
const latestMetaBlock = Number(latestMeta?.messageRef?.blockNumber);
|
|
||||||
const latest = Number.isFinite(latestMetaBlock) && (!Number.isFinite(latestMessageBlock) || latestMetaBlock > latestMessageBlock)
|
|
||||||
? latestMeta
|
|
||||||
: latestMessage;
|
|
||||||
const latestBlockNumber = Number(latest?.messageRef?.blockNumber);
|
|
||||||
const latestBlockHash = normalizeHex32(latest?.messageRef?.blockHash, '');
|
|
||||||
const latestLineStep = resolveLatestLineStep(latest);
|
|
||||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
|
||||||
prevLineNumber = latestBlockNumber;
|
|
||||||
prevLineHashHex = latestBlockHash;
|
|
||||||
thisLineNumber = latest?.kind === 'created'
|
|
||||||
? 0
|
|
||||||
: Number.isFinite(latestLineStep)
|
|
||||||
? Math.max(0, latestLineStep + 1)
|
|
||||||
: (latestBlockNumber === lineCode ? 0 : 1);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fallback to root anchor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latest = await this.findLatestOwnLineBlock({
|
||||||
|
blockchainName,
|
||||||
|
msgType: MSG_TYPE_TEXT,
|
||||||
|
subTypes: [
|
||||||
|
MSG_SUBTYPE_TEXT_POST,
|
||||||
|
MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||||
|
MSG_SUBTYPE_TEXT_REPOST,
|
||||||
|
MSG_SUBTYPE_TEXT_CHANNEL_META,
|
||||||
|
MSG_SUBTYPE_TEXT_ENTRYPOINT,
|
||||||
|
MSG_SUBTYPE_TEXT_EXERCISE,
|
||||||
|
MSG_SUBTYPE_TEXT_SERVICE,
|
||||||
|
MSG_SUBTYPE_TEXT_COURSE,
|
||||||
|
],
|
||||||
|
lineCode,
|
||||||
|
fixedLine: false,
|
||||||
|
stopAtBlock: lineCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineTail = latest
|
||||||
|
? {
|
||||||
|
prevLineNumber: latest.blockNumber,
|
||||||
|
prevLineHashHex: latest.blockHash,
|
||||||
|
thisLineNumber: latest.lineSequence + 1,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
prevLineNumber: lineCode,
|
||||||
|
prevLineHashHex: rootHashHex,
|
||||||
|
thisLineNumber: 0,
|
||||||
|
};
|
||||||
|
this.lineTailCache.set(cacheKey, lineTail);
|
||||||
return {
|
return {
|
||||||
ownerBlockchainName,
|
ownerBlockchainName,
|
||||||
lineCode,
|
lineCode,
|
||||||
rootHashHex,
|
rootHashHex,
|
||||||
prevLineNumber,
|
|
||||||
prevLineHashHex,
|
|
||||||
thisLineNumber,
|
|
||||||
channelSlug,
|
channelSlug,
|
||||||
|
...lineTail,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rememberChannelLineTail(blockchainName, lineCode, payload, currentSequence) {
|
||||||
|
const acceptedNumber = Number(payload?.serverLastGlobalNumber);
|
||||||
|
const acceptedHash = normalizeHex32(payload?.serverLastGlobalHash, ZERO64);
|
||||||
|
if (!Number.isInteger(acceptedNumber) || acceptedNumber < 0 || acceptedHash === ZERO64) return;
|
||||||
|
this.lineTailCache.set(`${String(blockchainName || '').trim()}|channel:${Number(lineCode)}`, {
|
||||||
|
prevLineNumber: acceptedNumber,
|
||||||
|
prevLineHashHex: acceptedHash,
|
||||||
|
thisLineNumber: Number(currentSequence) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async addBlockTextPost({ login, channel, text, storagePwd, msgSubType = MSG_SUBTYPE_TEXT_POST }) {
|
async addBlockTextPost({ login, channel, text, storagePwd, msgSubType = MSG_SUBTYPE_TEXT_POST }) {
|
||||||
const cleanLogin = (login || '').trim();
|
const cleanLogin = (login || '').trim();
|
||||||
if (!cleanLogin) throw new Error('Missing login');
|
if (!cleanLogin) throw new Error('Missing login');
|
||||||
@@ -2462,9 +2545,9 @@ export class AuthService {
|
|||||||
const selector = channel || {};
|
const selector = channel || {};
|
||||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||||
const root = Number(selector?.channelRootBlockNumber);
|
const root = Number(selector?.channelRootBlockNumber);
|
||||||
const key = `text-post:${cleanLogin}:${owner}:${root}:${cleanSubType}:${cleanText}`;
|
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||||
|
|
||||||
return this.runWriteLocked(key, async () => {
|
return this.runLineWriteLocked(key, async () => {
|
||||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
|
|
||||||
@@ -2487,6 +2570,7 @@ export class AuthService {
|
|||||||
bodyBytes,
|
bodyBytes,
|
||||||
channelSlug: tail.channelSlug,
|
channelSlug: tail.channelSlug,
|
||||||
});
|
});
|
||||||
|
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
@@ -2506,9 +2590,9 @@ export class AuthService {
|
|||||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||||
const root = Number(selector?.channelRootBlockNumber);
|
const root = Number(selector?.channelRootBlockNumber);
|
||||||
const metaText = composeChannelMetaText({ title, description, avatar });
|
const metaText = composeChannelMetaText({ title, description, avatar });
|
||||||
const key = `channel-meta:${cleanLogin}:${owner}:${root}:${metaText}`;
|
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||||
|
|
||||||
return this.runWriteLocked(key, async () => {
|
return this.runLineWriteLocked(key, async () => {
|
||||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
const tail = await this.resolveChannelLineTail({ login: cleanLogin, blockchainName, channel: selector });
|
const tail = await this.resolveChannelLineTail({ login: cleanLogin, blockchainName, channel: selector });
|
||||||
@@ -2529,6 +2613,7 @@ export class AuthService {
|
|||||||
bodyBytes,
|
bodyBytes,
|
||||||
channelSlug: tail.channelSlug,
|
channelSlug: tail.channelSlug,
|
||||||
});
|
});
|
||||||
|
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
@@ -3161,21 +3246,65 @@ export class AuthService {
|
|||||||
if (!cleanLogin || !cleanParam) throw new Error('Не переданы login/param.');
|
if (!cleanLogin || !cleanParam) throw new Error('Не переданы login/param.');
|
||||||
if (!cleanValue) throw new Error('Значение параметра не может быть пустым.');
|
if (!cleanValue) throw new Error('Значение параметра не может быть пустым.');
|
||||||
if (!storagePwd) throw new Error('Не передан storagePwd для подписи AddBlock.');
|
if (!storagePwd) throw new Error('Не передан storagePwd для подписи AddBlock.');
|
||||||
const bodyBytes = makeUserParamBodyBytes({
|
|
||||||
lineCode: 0,
|
return this.runLineWriteLocked(`profile-line:${cleanLogin}`, async () => {
|
||||||
prevLineNumber: -1,
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
prevLineHashHex: ZERO_HASH_HEX,
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
thisLineNumber: -1,
|
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'profile' });
|
||||||
key: cleanParam,
|
const bodyBytes = makeUserParamBodyBytes({
|
||||||
value: cleanValue,
|
prevProfileBlockNumber: tail.prevLineNumber,
|
||||||
|
prevProfileBlockHashHex: tail.prevLineHashHex,
|
||||||
|
profileSequence: tail.thisLineNumber,
|
||||||
|
key: cleanParam,
|
||||||
|
value: cleanValue,
|
||||||
|
});
|
||||||
|
const payload = await this.addBlockSigned({
|
||||||
|
login: cleanLogin,
|
||||||
|
storagePwd,
|
||||||
|
msgType: MSG_TYPE_USER_PROFILE,
|
||||||
|
msgSubType: 1,
|
||||||
|
msgVersion: 1,
|
||||||
|
bodyBytes,
|
||||||
|
});
|
||||||
|
this.rememberLineTail(blockchainName, 'profile', payload, tail.thisLineNumber);
|
||||||
|
return payload;
|
||||||
});
|
});
|
||||||
return this.addBlockSigned({
|
}
|
||||||
login: cleanLogin,
|
|
||||||
storagePwd,
|
async addBlockConnectionTarget({
|
||||||
msgType: 4,
|
login,
|
||||||
msgSubType: 1,
|
storagePwd,
|
||||||
msgVersion: 1,
|
subType,
|
||||||
bodyBytes,
|
toLogin,
|
||||||
|
toForkNumber,
|
||||||
|
toBlockNumber,
|
||||||
|
toBlockHashHex,
|
||||||
|
}) {
|
||||||
|
const cleanLogin = String(login || '').trim();
|
||||||
|
if (!cleanLogin) throw new Error('Missing login for CONNECTION');
|
||||||
|
return this.runLineWriteLocked(`connections-line:${cleanLogin}`, async () => {
|
||||||
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
|
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'connections' });
|
||||||
|
const bodyBytes = makeConnectionBodyBytes({
|
||||||
|
prevConnectionBlockNumber: tail.prevLineNumber,
|
||||||
|
prevConnectionBlockHashHex: tail.prevLineHashHex,
|
||||||
|
connectionSequence: tail.thisLineNumber,
|
||||||
|
toLogin,
|
||||||
|
toForkNumber,
|
||||||
|
toBlockNumber,
|
||||||
|
toBlockHashHex,
|
||||||
|
});
|
||||||
|
const payload = await this.addBlockSigned({
|
||||||
|
login: cleanLogin,
|
||||||
|
storagePwd,
|
||||||
|
msgType: MSG_TYPE_CONNECTION,
|
||||||
|
msgSubType: Number(subType),
|
||||||
|
msgVersion: 1,
|
||||||
|
bodyBytes,
|
||||||
|
});
|
||||||
|
this.rememberLineTail(blockchainName, 'connections', payload, tail.thisLineNumber);
|
||||||
|
return payload;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3190,9 +3319,6 @@ export class AuthService {
|
|||||||
throw new Error('Нельзя создать связь на самого себя.');
|
throw new Error('Нельзя создать связь на самого себя.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.getUser(cleanLogin);
|
|
||||||
if (user?.exists === false) throw new Error('Текущий пользователь не найден.');
|
|
||||||
|
|
||||||
const targetUser = await this.getUser(cleanToLogin);
|
const targetUser = await this.getUser(cleanToLogin);
|
||||||
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
|
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
|
||||||
const targetBlockchainName = String(targetUser.blockchainName || '').trim();
|
const targetBlockchainName = String(targetUser.blockchainName || '').trim();
|
||||||
@@ -3200,24 +3326,15 @@ export class AuthService {
|
|||||||
if (!targetForkNumber) throw new Error('Не удалось определить fork пользователя цели.');
|
if (!targetForkNumber) throw new Error('Не удалось определить fork пользователя цели.');
|
||||||
const targetHeaderHash = await this.resolveHeaderHashForBlockchain(targetBlockchainName);
|
const targetHeaderHash = await this.resolveHeaderHashForBlockchain(targetBlockchainName);
|
||||||
if (targetHeaderHash === ZERO64) throw new Error('HEADER пользователя цели не найден.');
|
if (targetHeaderHash === ZERO64) throw new Error('HEADER пользователя цели не найден.');
|
||||||
const bodyBytes = makeConnectionBodyBytes({
|
return this.addBlockConnectionTarget({
|
||||||
lineCode: 0,
|
login: cleanLogin,
|
||||||
prevLineNumber: -1,
|
storagePwd,
|
||||||
prevLineHashHex: ZERO_HASH_HEX,
|
subType: cleanSubType,
|
||||||
thisLineNumber: -1,
|
|
||||||
toLogin: cleanToLogin,
|
toLogin: cleanToLogin,
|
||||||
toForkNumber: targetForkNumber,
|
toForkNumber: targetForkNumber,
|
||||||
toBlockNumber: 0,
|
toBlockNumber: 0,
|
||||||
toBlockHashHex: targetHeaderHash,
|
toBlockHashHex: targetHeaderHash,
|
||||||
});
|
});
|
||||||
return this.addBlockSigned({
|
|
||||||
login: cleanLogin,
|
|
||||||
storagePwd,
|
|
||||||
msgType: 3,
|
|
||||||
msgSubType: cleanSubType,
|
|
||||||
msgVersion: 1,
|
|
||||||
bodyBytes,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,30 @@ function readFrameTimestampMs(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(42, false);
|
||||||
return Number(seconds * 1000n);
|
return Number(seconds * 1000n);
|
||||||
}
|
}
|
||||||
function makeForkBody({ oldBlockchainKeyB64, forkPointBlock, forkPointHash, forkPointTimestampMs, parentTipBlock, parentTipHash, parentTipTimestampMs, reasonCode, comment }) {
|
function readFrameType(frame) {
|
||||||
|
if (!(frame instanceof Uint8Array) || frame.length < 56) throw new Error('Некорректный SHiNE Frame');
|
||||||
|
const view=new DataView(frame.buffer,frame.byteOffset,frame.byteLength);
|
||||||
|
return { type:view.getUint16(50,false), subType:view.getUint16(52,false) };
|
||||||
|
}
|
||||||
|
function readExplicitLinePrefix(frame) {
|
||||||
|
if (!(frame instanceof Uint8Array) || frame.length < 100) throw new Error('Некорректный line Frame');
|
||||||
|
const view=new DataView(frame.buffer,frame.byteOffset,frame.byteLength);
|
||||||
|
return {
|
||||||
|
lineCode:view.getInt32(56,false),
|
||||||
|
prevLineNumber:view.getInt32(60,false),
|
||||||
|
prevLineHash:bytesToHex(frame.slice(64,96)),
|
||||||
|
lineSequence:view.getInt32(96,false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function makeForkBody({ lineCode=0, prevLineNumber, prevLineHash, lineSequence, oldBlockchainKeyB64, forkPointBlock, forkPointHash, forkPointTimestampMs, parentTipBlock, parentTipHash, parentTipTimestampMs, reasonCode, comment }) {
|
||||||
const oldKey=base64ToBytes(oldBlockchainKeyB64); if(oldKey.length!==32) throw new Error('Некорректный старый blockchain key');
|
const oldKey=base64ToBytes(oldBlockchainKeyB64); if(oldKey.length!==32) throw new Error('Некорректный старый blockchain key');
|
||||||
const c=TE.encode(normalizeComment(comment)); if(c.length>1024) throw new Error('Комментарий больше 1024 UTF-8 байт');
|
const c=TE.encode(normalizeComment(comment)); if(c.length>1024) throw new Error('Комментарий больше 1024 UTF-8 байт');
|
||||||
const discarded=Number(parentTipBlock)-Number(forkPointBlock);
|
const discarded=Number(parentTipBlock)-Number(forkPointBlock);
|
||||||
if(discarded<0) throw new Error('Некорректная точка fork');
|
if(discarded<0) throw new Error('Некорректная точка fork');
|
||||||
return concat(oldKey,be32(forkPointBlock),hexToBytes(forkPointHash),be64(forkPointTimestampMs),be32(parentTipBlock),hexToBytes(parentTipHash),be64(parentTipTimestampMs),be32(discarded),Uint8Array.of(Number(reasonCode)),be16(c.length),c);
|
if(Number(lineCode)!==0 || !Number.isInteger(Number(prevLineNumber)) || Number(prevLineNumber)<0 || !Number.isInteger(Number(lineSequence)) || Number(lineSequence)<1) {
|
||||||
|
throw new Error('Некорректная TECH line для FORK');
|
||||||
|
}
|
||||||
|
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, blockNumber, type, subType, version=1, body, timestampMs=Date.now() }) {
|
||||||
const bodyBytes=body || new Uint8Array(0);
|
const bodyBytes=body || new Uint8Array(0);
|
||||||
@@ -75,6 +93,24 @@ export class KeyRotationClient {
|
|||||||
return assertOk(await this.auth.ws.request('KeyRotationAddBlock', { blockNumber, prevBlockHash, blockBytesB64 }, 30000), 'KeyRotationAddBlock');
|
return assertOk(await this.auth.ws.request('KeyRotationAddBlock', { blockNumber, prevBlockHash, blockBytesB64 }, 30000), 'KeyRotationAddBlock');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #resolveTechLineTail(source, cutoff) {
|
||||||
|
for(let n=Number(cutoff);n>0;n--) {
|
||||||
|
const block=await this.getBlock(source,n);
|
||||||
|
const parsed=parseAns104DataItem(base64ToBytes(block.blockBytesB64));
|
||||||
|
const kind=readFrameType(parsed.data);
|
||||||
|
if(kind.type!==0 || (kind.subType!==1 && kind.subType!==TECH_FORK_SUBTYPE)) continue;
|
||||||
|
const line=readExplicitLinePrefix(parsed.data);
|
||||||
|
if(line.lineCode!==0 || line.lineSequence<1) throw new Error(`Некорректная TECH line в блоке #${n}`);
|
||||||
|
return {
|
||||||
|
prevLineNumber:n,
|
||||||
|
prevLineHash:String(block.blockHash||''),
|
||||||
|
lineSequence:line.lineSequence+1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const header=await this.getBlock(source,0);
|
||||||
|
return { prevLineNumber:0, prevLineHash:String(header.blockHash||''), lineSequence:1 };
|
||||||
|
}
|
||||||
|
|
||||||
async copyCandidateChain({ rotation, newBundle, onProgress=()=>{} }) {
|
async copyCandidateChain({ rotation, newBundle, onProgress=()=>{} }) {
|
||||||
const cutoff=Number(rotation.forkFromBlock); const source=String(rotation.sourceBlockchainName||'');
|
const cutoff=Number(rotation.forkFromBlock); const source=String(rotation.sourceBlockchainName||'');
|
||||||
if(!source || !Number.isInteger(cutoff) || cutoff<0) throw new Error('Некорректное состояние ротации');
|
if(!source || !Number.isInteger(cutoff) || cutoff<0) throw new Error('Некорректное состояние ротации');
|
||||||
@@ -100,7 +136,12 @@ export class KeyRotationClient {
|
|||||||
const tipSource=tipBlock===cutoff ? forkSource : await this.getBlock(source,tipBlock);
|
const tipSource=tipBlock===cutoff ? forkSource : await this.getBlock(source,tipBlock);
|
||||||
const forkParsed=parseAns104DataItem(base64ToBytes(forkSource.blockBytesB64));
|
const forkParsed=parseAns104DataItem(base64ToBytes(forkSource.blockBytesB64));
|
||||||
const tipParsed=parseAns104DataItem(base64ToBytes(tipSource.blockBytesB64));
|
const tipParsed=parseAns104DataItem(base64ToBytes(tipSource.blockBytesB64));
|
||||||
|
const techTail=await this.#resolveTechLineTail(source,cutoff);
|
||||||
const forkBody=makeForkBody({
|
const forkBody=makeForkBody({
|
||||||
|
lineCode:0,
|
||||||
|
prevLineNumber:techTail.prevLineNumber,
|
||||||
|
prevLineHash:techTail.prevLineHash,
|
||||||
|
lineSequence:techTail.lineSequence,
|
||||||
oldBlockchainKeyB64:rotation.oldBlockchainKey,
|
oldBlockchainKeyB64:rotation.oldBlockchainKey,
|
||||||
forkPointBlock:cutoff,
|
forkPointBlock:cutoff,
|
||||||
forkPointHash:rotation.forkFromHash,
|
forkPointHash:rotation.forkFromHash,
|
||||||
@@ -111,9 +152,9 @@ export class KeyRotationClient {
|
|||||||
reasonCode:rotation.reasonCode,
|
reasonCode:rotation.reasonCode,
|
||||||
comment:rotation.comment,
|
comment:rotation.comment,
|
||||||
});
|
});
|
||||||
const techTimestampMs=Number(rotation.createdAtMs)||Number(parentTipTimestampMs)+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 techFrame=makeFrame({ prevHash:rotation.forkFromHash, blockNumber:cutoff+1, type:0, subType:TECH_FORK_SUBTYPE, version:1, body:forkBody, timestampMs:techTimestampMs });
|
||||||
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:'App',value:'test5590'}] });
|
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:'App',value:'test5590'},{name:'t',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 });
|
||||||
}
|
}
|
||||||
@@ -160,17 +201,12 @@ export class KeyRotationClient {
|
|||||||
}
|
}
|
||||||
const state=await this.notifyPdaRotation(signature);
|
const state=await this.notifyPdaRotation(signature);
|
||||||
if(storagePwd) {
|
if(storagePwd) {
|
||||||
await updateEncryptedUserSecrets(login, storagePwd, (current)=>{
|
await updateEncryptedUserSecrets(login, storagePwd, (current)=>({
|
||||||
const next={
|
...(current||{}),
|
||||||
...(current||{}),
|
rootKey:newBundle.rootPair.privatePkcs8B64,
|
||||||
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
||||||
clientKey:newBundle.clientPair.privatePkcs8B64,
|
clientKey:newBundle.clientPair.privatePkcs8B64,
|
||||||
};
|
}));
|
||||||
// Arweave SAWD-v1 привязан к blockchain key: после ротации старый JWK-cache недействителен.
|
|
||||||
delete next.arweaveWallet;
|
|
||||||
delete next.rootKey;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2430,7 +2430,7 @@ var solanaWeb3=function(exports){"use strict";function getDefaultExportFromCjs(x
|
|||||||
const gateway = baseUrl(gatewayBaseUrl);
|
const gateway = baseUrl(gatewayBaseUrl);
|
||||||
const ownerAddress = await arweaveOwnerAddressFromBlockchainKey(blockchainKey);
|
const ownerAddress = await arweaveOwnerAddressFromBlockchainKey(blockchainKey);
|
||||||
const tags = [{ name: 'App', values: [SHINE_ARWEAVE_APP_TAG] }];
|
const tags = [{ name: 'App', values: [SHINE_ARWEAVE_APP_TAG] }];
|
||||||
if (channelName) tags.push({ name: 'c', values: [channelName] });
|
if (channelName) tags.push({ name: 'c_test5590', values: [channelName] });
|
||||||
const query = `query($owners:[String!],$tags:[TagFilter!],$after:String){transactions(first:100,after:$after,owners:$owners,tags:$tags,sort:HEIGHT_DESC){pageInfo{hasNextPage}edges{cursor node{id owner{address key} block{height timestamp} bundledIn{id} tags{name value}}}}}`;
|
const query = `query($owners:[String!],$tags:[TagFilter!],$after:String){transactions(first:100,after:$after,owners:$owners,tags:$tags,sort:HEIGHT_DESC){pageInfo{hasNextPage}edges{cursor node{id owner{address key} block{height timestamp} bundledIn{id} tags{name value}}}}}`;
|
||||||
let after = null;
|
let after = null;
|
||||||
let page = 0;
|
let page = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user