279 lines
11 KiB
Java
279 lines
11 KiB
Java
package blockchain;
|
||
|
||
import blockchain.body.BodyRecord;
|
||
import blockchain.body.BodyRecordParser;
|
||
|
||
import java.nio.ByteBuffer;
|
||
import java.nio.ByteOrder;
|
||
import java.time.Instant;
|
||
import java.util.Arrays;
|
||
import java.util.Objects;
|
||
|
||
/**
|
||
* BchBlockEntry — универсальный блок нового формата.
|
||
*
|
||
* RAW (BigEndian) = preimage:
|
||
* [32] prevHash32 (SHA-256) hash предыдущего блока (цепочка)
|
||
* [4] blockSize (int) = размер preimage (в байтах), БЕЗ signature64
|
||
* [4] blockNumber (int) глобальный номер блока (>=0)
|
||
* [8] timestamp (long) unix seconds
|
||
*
|
||
* [2] type (short) тип сообщения
|
||
* [2] subType (short) подтип сообщения
|
||
* [2] version (short) версия формата сообщения
|
||
*
|
||
* [N] bodyBytes (bytes) тело сообщения (БЕЗ type/subType/version)
|
||
*
|
||
* TAIL (НЕ входит в blockSize):
|
||
* [64] signature64 (Ed25519) подпись над hash32
|
||
*
|
||
* hash32 ВНУТРИ БЛОКА НЕ ХРАНИМ.
|
||
* hash32 вычисляется при парсинге:
|
||
* preimage = первые blockSize байт
|
||
* hash32 = SHA-256(preimage)
|
||
*/
|
||
public final class BchBlockEntry {
|
||
|
||
public static final int SIGNATURE_LEN = 64;
|
||
public static final int HASH_LEN = 32;
|
||
|
||
/**
|
||
* Максимальный допустимый размер блока (preimage+signature), чтобы не уложить сервер по памяти/диску.
|
||
* 4 МБ — нормальный “потолок” под тексты/метаданные, и при этом защищает от мусора/атаки.
|
||
*/
|
||
public static final int MAX_BLOCK_FULL_BYTES = 4 * 1024 * 1024;
|
||
|
||
/**
|
||
* Насколько блок может “обгонять” текущее время (защита от кривых часов/вбросов).
|
||
* Если timestamp больше now + 60 сек — блок считаем неверным.
|
||
*/
|
||
public static final long MAX_FUTURE_SECONDS = 60;
|
||
|
||
/** Размер фиксированного RAW-заголовка без body */
|
||
public static final int RAW_HEADER_SIZE =
|
||
32 // prevHash32
|
||
+ 4 // blockSize
|
||
+ 4 // blockNumber
|
||
+ 8 // timestamp
|
||
+ 2 // type
|
||
+ 2 // subType
|
||
+ 2; // version
|
||
|
||
// --- HEADER (RAW) ---
|
||
public final byte[] prevHash32; // 32
|
||
public final int blockSize; // preimage size
|
||
public final int blockNumber; // >=0
|
||
public final long timestamp;
|
||
public final short type;
|
||
public final short subType;
|
||
public final short version;
|
||
|
||
// --- BODY (RAW) ---
|
||
public final byte[] bodyBytes;
|
||
|
||
/** Распарсенное тело (создаётся сразу при парсинге блока). */
|
||
public final BodyRecord body;
|
||
|
||
// --- TAIL ---
|
||
private final byte[] signature64; // 64
|
||
|
||
// --- derived ---
|
||
private final byte[] hash32; // 32, computed
|
||
private final byte[] preimage; // blockSize bytes
|
||
private final byte[] fullBytes; // preimage + signature
|
||
|
||
/* ===================================================================== */
|
||
/* ====================== Конструктор из байт ========================== */
|
||
/* ===================================================================== */
|
||
|
||
public BchBlockEntry(byte[] fullBytes) {
|
||
Objects.requireNonNull(fullBytes, "fullBytes == null");
|
||
|
||
if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN) {
|
||
throw new IllegalArgumentException("Block too short");
|
||
}
|
||
if (fullBytes.length > MAX_BLOCK_FULL_BYTES) {
|
||
throw new IllegalArgumentException("Block too large: " + fullBytes.length + " > " + MAX_BLOCK_FULL_BYTES);
|
||
}
|
||
|
||
ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN);
|
||
|
||
this.prevHash32 = new byte[32];
|
||
bb.get(this.prevHash32);
|
||
|
||
this.blockSize = bb.getInt();
|
||
if (blockSize < RAW_HEADER_SIZE) {
|
||
throw new IllegalArgumentException("blockSize too small: " + blockSize);
|
||
}
|
||
if (blockSize + SIGNATURE_LEN != fullBytes.length) {
|
||
throw new IllegalArgumentException("blockSize mismatch: blockSize=" + blockSize + " fullLen=" + fullBytes.length);
|
||
}
|
||
if (blockSize + SIGNATURE_LEN > MAX_BLOCK_FULL_BYTES) {
|
||
throw new IllegalArgumentException("Block too large by blockSize: " + (blockSize + SIGNATURE_LEN) + " > " + MAX_BLOCK_FULL_BYTES);
|
||
}
|
||
|
||
this.blockNumber = bb.getInt();
|
||
if (this.blockNumber < 0) {
|
||
throw new IllegalArgumentException("blockNumber < 0: " + this.blockNumber);
|
||
}
|
||
|
||
this.timestamp = bb.getLong();
|
||
|
||
// запрет “в будущее” больше чем на 1 минуту
|
||
long now = Instant.now().getEpochSecond();
|
||
if (this.timestamp > now + MAX_FUTURE_SECONDS) {
|
||
throw new IllegalArgumentException("timestamp is too far in future: ts=" + this.timestamp + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS);
|
||
}
|
||
|
||
this.type = bb.getShort();
|
||
this.subType = bb.getShort();
|
||
this.version = bb.getShort();
|
||
|
||
int bodyLen = blockSize - RAW_HEADER_SIZE;
|
||
if (bodyLen < 0) throw new IllegalArgumentException("Invalid body length: " + bodyLen);
|
||
|
||
this.bodyBytes = new byte[bodyLen];
|
||
bb.get(this.bodyBytes);
|
||
|
||
this.signature64 = new byte[SIGNATURE_LEN];
|
||
bb.get(this.signature64);
|
||
|
||
// preimage = первые blockSize байт
|
||
this.preimage = Arrays.copyOfRange(fullBytes, 0, blockSize);
|
||
|
||
// hash32 = sha256(preimage)
|
||
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||
|
||
// parse body по header.type/subType/version + ОБЯЗАТЕЛЬНЫЙ check()
|
||
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||
|
||
this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length);
|
||
|
||
// запрет мусора
|
||
if (bb.remaining() != 0) {
|
||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||
}
|
||
}
|
||
|
||
/* ===================================================================== */
|
||
/* ====================== Конструктор сборки ============================ */
|
||
/* ===================================================================== */
|
||
|
||
public BchBlockEntry(byte[] prevHash32,
|
||
int blockNumber,
|
||
long timestamp,
|
||
short type,
|
||
short subType,
|
||
short version,
|
||
byte[] bodyBytes,
|
||
byte[] signature64) {
|
||
|
||
Objects.requireNonNull(prevHash32, "prevHash32 == null");
|
||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||
Objects.requireNonNull(signature64, "signature64 == null");
|
||
|
||
if (prevHash32.length != 32) throw new IllegalArgumentException("prevHash32 != 32");
|
||
if (signature64.length != SIGNATURE_LEN) throw new IllegalArgumentException("signature64 != 64");
|
||
|
||
if (blockNumber < 0) {
|
||
throw new IllegalArgumentException("blockNumber < 0: " + blockNumber);
|
||
}
|
||
|
||
// запрет “в будущее” больше чем на 1 минуту
|
||
long now = Instant.now().getEpochSecond();
|
||
if (timestamp > now + MAX_FUTURE_SECONDS) {
|
||
throw new IllegalArgumentException("timestamp is too far in future: ts=" + timestamp + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS);
|
||
}
|
||
|
||
this.prevHash32 = Arrays.copyOf(prevHash32, 32);
|
||
this.blockNumber = blockNumber;
|
||
this.timestamp = timestamp;
|
||
this.type = type;
|
||
this.subType = subType;
|
||
this.version = version;
|
||
this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
|
||
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
|
||
|
||
this.blockSize = RAW_HEADER_SIZE + this.bodyBytes.length;
|
||
|
||
int fullLen = this.blockSize + SIGNATURE_LEN;
|
||
if (fullLen > MAX_BLOCK_FULL_BYTES) {
|
||
throw new IllegalArgumentException("Block too large: " + fullLen + " > " + MAX_BLOCK_FULL_BYTES);
|
||
}
|
||
|
||
// parse body по header + ОБЯЗАТЕЛЬНЫЙ check()
|
||
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||
|
||
// build preimage
|
||
ByteBuffer pre = ByteBuffer.allocate(blockSize).order(ByteOrder.BIG_ENDIAN);
|
||
pre.put(this.prevHash32);
|
||
pre.putInt(this.blockSize);
|
||
pre.putInt(this.blockNumber);
|
||
pre.putLong(this.timestamp);
|
||
pre.putShort(this.type);
|
||
pre.putShort(this.subType);
|
||
pre.putShort(this.version);
|
||
pre.put(this.bodyBytes);
|
||
|
||
this.preimage = pre.array();
|
||
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||
|
||
ByteBuffer full = ByteBuffer.allocate(blockSize + SIGNATURE_LEN).order(ByteOrder.BIG_ENDIAN);
|
||
full.put(this.preimage);
|
||
full.put(this.signature64);
|
||
this.fullBytes = full.array();
|
||
}
|
||
|
||
public byte[] getPreimageBytes() {
|
||
return Arrays.copyOf(preimage, preimage.length);
|
||
}
|
||
|
||
public byte[] getSignature64() {
|
||
return Arrays.copyOf(signature64, SIGNATURE_LEN);
|
||
}
|
||
|
||
public byte[] getHash32() {
|
||
return Arrays.copyOf(hash32, HASH_LEN);
|
||
}
|
||
|
||
public byte[] toBytes() {
|
||
return Arrays.copyOf(fullBytes, fullBytes.length);
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
String timeIso;
|
||
try {
|
||
timeIso = Instant.ofEpochSecond(timestamp).toString();
|
||
} catch (Exception e) {
|
||
timeIso = "некорректныйTimestamp";
|
||
}
|
||
|
||
return "BchBlockEntry{"
|
||
+ "HDR{"
|
||
+ "blockSize=" + blockSize
|
||
+ ", blockNumber=" + blockNumber
|
||
+ ", timestamp=" + timestamp + " (" + timeIso + ")"
|
||
+ ", type=" + (type & 0xFFFF)
|
||
+ ", subType=" + (subType & 0xFFFF)
|
||
+ ", version=" + (version & 0xFFFF)
|
||
+ ", prevHash32(hex)=" + toHex(prevHash32)
|
||
+ "}"
|
||
+ ", BODY{len=" + (bodyBytes == null ? -1 : bodyBytes.length) + "}"
|
||
+ ", TAIL{signature64(hex)=" + toHex(signature64) + "}"
|
||
+ ", DERIVED{hash32(hex)=" + toHex(hash32) + "}"
|
||
+ "}";
|
||
}
|
||
|
||
private static String toHex(byte[] bytes) {
|
||
if (bytes == null) return "null";
|
||
char[] HEX = "0123456789abcdef".toCharArray();
|
||
char[] out = new char[bytes.length * 2];
|
||
for (int i = 0; i < bytes.length; i++) {
|
||
int v = bytes[i] & 0xFF;
|
||
out[i * 2] = HEX[v >>> 4];
|
||
out[i * 2 + 1] = HEX[v & 0x0F];
|
||
}
|
||
return new String(out);
|
||
}
|
||
} |