CreateChannel: оставить единый актуальный формат, убрать legacy v2/v3

This commit is contained in:
AidarKC 2026-05-13 01:22:07 +03:00
parent e95f65ac78
commit ddeaf82bfd
7 changed files with 51 additions and 183 deletions

View File

@ -1,7 +1,7 @@
# Типы каналов и CreateChannel v3
# Типы каналов и CreateChannel
## 1. Формат `CreateChannelBody v3`
Формат `TECH_CREATE_CHANNEL` поддерживает `version=3` и включает:
## 1. Формат `CreateChannelBody`
Формат `TECH_CREATE_CHANNEL` поддерживает единственный текущий `version=1` и включает:
1. line-поля канала (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`);
2. `channelName`;

View File

@ -2,7 +2,7 @@
## 2026-05-13 00:02:32 +0300
- Базовый коммит-ориентир: `f63f40f1eb2f`.
- Добавлен `CreateChannelBody v3` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`.
- Добавлен текущий формат `CreateChannelBody` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`.
- Зафиксированы типы каналов: `0=stories`, `1=public`, `100=personal`, `200=group`.
- Серверная уникальность имени канала изменена на `owner + type + name(slug)`.
- Root-канал `0` переименован в `stories` на уровне API-чтения.

View File

@ -4,8 +4,8 @@
Этот набор файлов — актуальная документация по текущему формату блокчейна SHiNE для каналов, их типов и командных сообщений.
## Оглавление
1. [01_Channel_Types_and_CreateChannel_v3.md](./01_Channel_Types_and_CreateChannel_v3.md)
Формат `CreateChannelBody v3`, типы каналов, уникальность имён и правила `stories`.
1. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
Текущий формат `CreateChannelBody`, типы каналов, уникальность имён и правила `stories`.
2. [02_Channel_Commands.md](./02_Channel_Commands.md)
Командные сообщения в каналах: `/.desc`, `/.add`, `/.remove`.
3. [CHANGELOG.md](./CHANGELOG.md)

View File

@ -42,7 +42,7 @@ const MSG_SUBTYPE_REACTION_LIKE = 1;
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
const MSG_SUBTYPE_CONNECTION_UNFOLLOW = 31;
const CREATE_CHANNEL_BODY_VERSION = 3;
const CREATE_CHANNEL_BODY_VERSION = 1;
const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PUBLIC = 1;
const CHANNEL_TYPE_PERSONAL = 100;
@ -409,7 +409,7 @@ function normalizeChannelDescription(value) {
return text;
}
function makeCreateChannelBodyV3Bytes({
function makeCreateChannelBodyBytes({
lineCode,
prevLineNumber,
prevLineHashHex,
@ -1080,7 +1080,7 @@ export class AuthService {
msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgVersion: CREATE_CHANNEL_BODY_VERSION,
bodyBytes: makeCreateChannelBodyV3Bytes({
bodyBytes: makeCreateChannelBodyBytes({
lineCode: 0,
prevLineNumber,
prevLineHashHex,

View File

@ -16,15 +16,13 @@ public final class BodyRecordParser {
int v = version & 0xFFFF;
int st = subType & 0xFFFF;
// TECH supports Header v1 and CreateChannel v1/v2.
// TECH supports Header v1 and CreateChannel current format (ver=1).
if (t == (CreateChannelBody.TYPE & 0xFFFF)) {
if (st == (HeaderBody.SUBTYPE_COMPAT & 0xFFFF) && v == (HeaderBody.VER & 0xFFFF)) {
return new HeaderBody(subType, version, bodyBytes).check();
}
if (st == (CreateChannelBody.SUBTYPE & 0xFFFF)
&& (v == (CreateChannelBody.VER & 0xFFFF)
|| v == (CreateChannelBody.VER2 & 0xFFFF)
|| v == (CreateChannelBody.VER3 & 0xFFFF))) {
&& (v == (CreateChannelBody.VER & 0xFFFF))) {
return new CreateChannelBody(subType, version, bodyBytes).check();
}
throw new IllegalArgumentException(

View File

@ -11,25 +11,7 @@ import java.util.Objects;
/**
* TECH body for create channel.
*
* v1 body bytes:
* [4] lineCode
* [4] prevLineNumber
* [32] prevLineHash32
* [4] thisLineNumber
* [1] channelNameLen
* [N] channelName UTF-8
*
* v2 body bytes:
* [4] lineCode
* [4] prevLineNumber
* [32] prevLineHash32
* [4] thisLineNumber
* [1] channelNameLen
* [N] channelName UTF-8
* [2] channelDescriptionLen
* [M] channelDescription UTF-8 (0..200 bytes)
*
* v3 body bytes:
* body bytes:
* [4] lineCode
* [4] prevLineNumber
* [32] prevLineHash32
@ -45,13 +27,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
public static final short TYPE = 0;
public static final short VER = 1;
public static final short VER2 = 2;
public static final short VER3 = 3;
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
public static final int KEY_V2 = ((TYPE & 0xFFFF) << 16) | (VER2 & 0xFFFF);
public static final int KEY_V3 = ((TYPE & 0xFFFF) << 16) | (VER3 & 0xFFFF);
public static final short SUBTYPE = MsgSubType.TECH_CREATE_CHANNEL;
public static final short CHANNEL_TYPE_STORIES = 0;
@ -79,35 +55,30 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
public CreateChannelBody(short subType, short version, byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
this.subType = subType;
this.version = version;
int ver = this.version & 0xFFFF;
if (ver != (VER & 0xFFFF) && ver != (VER2 & 0xFFFF) && ver != (VER3 & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody version must be 1, 2 or 3, got=" + ver);
if (ver != (VER & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody version must be 1, got=" + ver);
}
if ((this.subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF));
}
if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1) {
if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1 + 2 + 4) {
throw new IllegalArgumentException("CreateChannelBody too short");
}
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();
int nameLen = Byte.toUnsignedInt(bb.get());
if (nameLen <= 0) throw new IllegalArgumentException("channelNameLen is 0");
if (bb.remaining() < nameLen) {
if (bb.remaining() < nameLen + 2 + 4) {
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " nameLen=" + nameLen);
}
@ -115,19 +86,13 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
bb.get(nameBytes);
this.channelName = new String(nameBytes, StandardCharsets.UTF_8);
if (ver == (VER2 & 0xFFFF) || ver == (VER3 & 0xFFFF)) {
if (bb.remaining() < 2) {
throw new IllegalArgumentException("CreateChannelBody v2/v3 missing channelDescriptionLen");
}
int descriptionLen = Short.toUnsignedInt(bb.getShort());
if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) {
throw new IllegalArgumentException("channelDescription utf8 len must be <=200");
}
if (bb.remaining() != descriptionLen) {
throw new IllegalArgumentException("CreateChannelBody v2 tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
if (bb.remaining() != descriptionLen + 4) {
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
}
if (descriptionLen == 0) {
this.channelDescription = "";
} else {
@ -135,46 +100,11 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
bb.get(descriptionBytes);
this.channelDescription = normalizeDescription(new String(descriptionBytes, StandardCharsets.UTF_8));
}
if (ver == (VER3 & 0xFFFF)) {
if (bb.remaining() < 4) {
throw new IllegalArgumentException("CreateChannelBody v3 missing channelTypeCode/channelTypeVersion");
}
this.channelTypeCode = bb.getShort();
this.channelTypeVersion = bb.getShort();
} else {
this.channelTypeCode = CHANNEL_TYPE_PUBLIC;
this.channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT;
}
if (bb.remaining() != 0) {
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
}
return;
}
this.channelDescription = "";
this.channelTypeCode = CHANNEL_TYPE_PUBLIC;
this.channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT;
if (bb.remaining() != 0) {
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
}
}
public CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, "", VER);
}
public CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription, VER2);
}
public CreateChannelBody(int lineCode,
@ -185,41 +115,15 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
String channelDescription,
short channelTypeCode,
short channelTypeVersion) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription,
channelTypeCode, channelTypeVersion, VER3);
}
private CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription,
short version) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription,
CHANNEL_TYPE_PUBLIC, CHANNEL_TYPE_VERSION_DEFAULT, version);
}
private CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription,
short channelTypeCode,
short channelTypeVersion,
short version) {
Objects.requireNonNull(channelName, "channelName == null");
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
this.subType = SUBTYPE;
this.version = version;
this.version = VER;
this.lineCode = lineCode;
this.prevLineNumber = prevLineNumber;
this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
this.thisLineNumber = thisLineNumber;
this.channelName = channelName;
this.channelDescription = channelDescription == null ? "" : channelDescription;
this.channelTypeCode = channelTypeCode;
@ -229,20 +133,14 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
@Override
public CreateChannelBody check() {
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)");
}
String normalizedName = normalizeDisplayName(channelName);
if (normalizedName.isEmpty()) {
throw new IllegalArgumentException("channelName is blank");
}
if (normalizedName.isEmpty()) throw new IllegalArgumentException("channelName is blank");
int cpLen = normalizedName.codePointCount(0, normalizedName.length());
if (cpLen > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("channelName length must be <=32");
}
if (cpLen > MAX_NAME_LENGTH) throw new IllegalArgumentException("channelName length must be <=32");
String normalizedDescription = normalizeDescription(channelDescription);
byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8);
@ -252,23 +150,12 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
int typeCode = Short.toUnsignedInt(channelTypeCode);
int typeVer = Short.toUnsignedInt(channelTypeVersion);
if (typeCode < 0 || typeCode > 0xFFFF) {
throw new IllegalArgumentException("channelTypeCode invalid");
}
if (typeVer < 0 || typeVer > 0xFFFF) {
throw new IllegalArgumentException("channelTypeVersion invalid");
}
if (prevLineNumber < 0) {
throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
}
if (prevLineHash32 == null || prevLineHash32.length != 32) {
throw new IllegalArgumentException("prevLineHash32 invalid");
}
if (thisLineNumber <= 0) {
throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
}
if (typeCode < 0 || typeCode > 0xFFFF) throw new IllegalArgumentException("channelTypeCode invalid");
if (typeVer < 0 || typeVer > 0xFFFF) throw new IllegalArgumentException("channelTypeVersion invalid");
if (prevLineNumber < 0) throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
if (thisLineNumber <= 0) throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
return this;
}
@ -288,53 +175,33 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
if (nameUtf8.length == 0 || nameUtf8.length > 255) {
throw new IllegalArgumentException("channelName utf8 len must be 1..255");
}
boolean isV2 = (version & 0xFFFF) == (VER2 & 0xFFFF);
boolean isV3 = (version & 0xFFFF) == (VER3 & 0xFFFF);
byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8);
if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
throw new IllegalArgumentException("channelDescription utf8 len must be <=200");
}
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length
+ ((isV2 || isV3) ? 2 + descriptionUtf8.length : 0)
+ (isV3 ? 4 : 0);
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4;
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
bb.putInt(lineCode);
bb.putInt(prevLineNumber);
bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
bb.putInt(thisLineNumber);
bb.put((byte) nameUtf8.length);
bb.put(nameUtf8);
if (isV2 || isV3) {
bb.putShort((short) (descriptionUtf8.length & 0xFFFF));
if (descriptionUtf8.length > 0) {
bb.put(descriptionUtf8);
}
}
if (isV3) {
if (descriptionUtf8.length > 0) bb.put(descriptionUtf8);
bb.putShort(channelTypeCode);
bb.putShort(channelTypeVersion);
}
return bb.array();
}
@Override
public int lineCode() { return lineCode; }
@Override
public int prevLineBlockGlobalNumber() { return prevLineNumber; }
@Override
public byte[] prevLineBlockHash32() {
return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32);
}
public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
@Override
public int lineSeq() { return thisLineNumber; }
}

View File

@ -105,7 +105,10 @@ public class IT_03_AddBlock_NoAuth {
sender1.send(new CreateChannelBody(
0, // lineCode TECH
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
"News"
"News",
"",
CreateChannelBody.CHANNEL_TYPE_PUBLIC,
CreateChannelBody.CHANNEL_TYPE_VERSION_DEFAULT
), t);
newsRootBlock = st1.lastBlockNumber(); // root канала = blockNumber этого CREATE_CHANNEL